返回

经典动态规划难题:416. 分割等和子集——探究解题思路

前端

动态规划,就像它的名字一样,充满着变化和规划的艺术。它擅长将看似复杂的问题,拆解成一系列更小、更易于解决的子问题。通过解决这些子问题,我们最终能够找到通往原始问题答案的路径。这种“化整为零”的策略,正是动态规划的核心魅力所在。

今天,我们来一起探索动态规划在解决实际问题中的应用,特别是如何用它来解决力扣上的一个经典题目——416. 分割等和子集。

这个问题的很简单:给你一个只包含正整数的非空数组,你需要判断能否将这个数组分割成两个子集,使得这两个子集的元素和相等。

例如,如果数组是 [1, 5, 11, 5],那么我们可以将其分割成 [1, 5, 5] 和 [11] 两个子集,它们的元素和都等于 11,所以答案是 True。

但如果数组是 [1, 2, 3, 5],我们就无法找到这样的两个子集,所以答案是 False。

要解决这个问题,我们可以借助动态规划的强大能力。动态规划的核心思想是建立一个表格(通常称为“状态转移表”或“DP 表”),用来存储子问题的解。通过逐步填充这个表格,我们最终可以得到原始问题的解。

在这个问题中,我们可以定义一个二维的 DP 表 dp,其中 dp[i][j] 表示能否用数组的前 i 个数字组成和为 j 的子集。

那么,如何填充这个 DP 表呢?我们可以根据以下规则进行:

  1. 初始化dp[0][0] = True,因为不使用任何数字可以组成和为 0 的子集。其他所有 dp[0][j]dp[i][0] 都初始化为 False
  2. 状态转移 :对于 dp[i][j],它有两个来源:
    • 如果不使用第 i 个数字,那么 dp[i][j] 的值就等于 dp[i-1][j]
    • 如果使用第 i 个数字,那么 dp[i][j] 的值就等于 dp[i-1][j-nums[i-1]],前提是 j >= nums[i-1]
  3. 最终结果 :最终,dp[n][sum/2] 就是我们想要的答案,其中 n 是数组的长度,sum 是数组所有元素的和。当然,如果 sum 是奇数,那么答案一定是 False,因为无法将奇数分成两个相等的整数。

下面,我们来看一个具体的例子,假设数组是 [1, 5, 11, 5]:

0 1 2 3 4 5 6 7 8 9 10 11
0 T F F F F F F F F F F F
1 T T F F F F F F F F F F
2 T T F F F T T F F F F F
3 T T F F F T T F F F F T
4 T T F F F T T F F F F T

最终,dp[4][11] 的值是 True,所以答案是 True

下面是用 Python 实现的代码:

def canPartition(nums):
    total_sum = sum(nums)
    if total_sum % 2 != 0:
        return False
    target = total_sum // 2
    n = len(nums)
    dp = [[False] * (target + 1) for _ in range(n + 1)]
    for i in range(n + 1):
        dp[i][0] = True
    for i in range(1, n + 1):
        for j in range(1, target + 1):
            if j < nums[i - 1]:
                dp[i][j] = dp[i - 1][j]
            else:
                dp[i][j] = dp[i - 1][j] or dp[i - 1][j - nums[i - 1]]
    return dp[n][target]

通过这段代码,我们可以清晰地看到动态规划的精髓:将问题分解成子问题,并利用子问题的解来构建最终的答案。

常见问题解答

  1. 动态规划和递归有什么区别?
    动态规划和递归都是解决问题的方法,但它们的核心思想不同。递归是将问题分解成规模更小的相同子问题,然后通过递归调用来解决这些子问题。而动态规划则是将问题分解成规模更小的不同子问题,并将子问题的解存储起来,避免重复计算。

  2. 动态规划一定比递归效率高吗?
    不一定。在某些情况下,递归的代码可能更简洁易懂。但如果存在大量的重复计算,那么动态规划的效率通常会更高。

  3. 如何判断一个问题是否适合用动态规划来解决?
    通常,如果一个问题具有以下两个特征,那么它就适合用动态规划来解决:

    • 最优子结构 :问题的最优解可以由子问题的最优解构成。
    • 重叠子问题 :在求解过程中,存在大量的重复计算。
  4. 动态规划的时间复杂度是多少?
    动态规划的时间复杂度通常与状态的数量和状态转移的复杂度有关。在本例中,状态的数量是 n * target,状态转移的复杂度是 O(1),所以总的时间复杂度是 O(n * target)。

  5. 除了分割等和子集问题,动态规划还能解决哪些问题?
    动态规划可以解决很多问题,例如:

    • 最长公共子序列问题
    • 背包问题
    • 编辑距离问题
    • 最短路径问题 等等。

希望这篇文章能够帮助你更好地理解动态规划,并掌握如何用它来解决实际问题。动态规划是一种非常强大的算法技术,它可以帮助我们解决很多看似复杂的问题。只要你掌握了它的核心思想,就能灵活运用它来解决各种各样的问题。