经典动态规划难题:416. 分割等和子集——探究解题思路
2024-02-25 03:55:51
动态规划,就像它的名字一样,充满着变化和规划的艺术。它擅长将看似复杂的问题,拆解成一系列更小、更易于解决的子问题。通过解决这些子问题,我们最终能够找到通往原始问题答案的路径。这种“化整为零”的策略,正是动态规划的核心魅力所在。
今天,我们来一起探索动态规划在解决实际问题中的应用,特别是如何用它来解决力扣上的一个经典题目——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 表呢?我们可以根据以下规则进行:
- 初始化 :
dp[0][0] = True
,因为不使用任何数字可以组成和为 0 的子集。其他所有dp[0][j]
和dp[i][0]
都初始化为False
。 - 状态转移 :对于
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]
。
- 如果不使用第
- 最终结果 :最终,
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]
通过这段代码,我们可以清晰地看到动态规划的精髓:将问题分解成子问题,并利用子问题的解来构建最终的答案。
常见问题解答
-
动态规划和递归有什么区别?
动态规划和递归都是解决问题的方法,但它们的核心思想不同。递归是将问题分解成规模更小的相同子问题,然后通过递归调用来解决这些子问题。而动态规划则是将问题分解成规模更小的不同子问题,并将子问题的解存储起来,避免重复计算。 -
动态规划一定比递归效率高吗?
不一定。在某些情况下,递归的代码可能更简洁易懂。但如果存在大量的重复计算,那么动态规划的效率通常会更高。 -
如何判断一个问题是否适合用动态规划来解决?
通常,如果一个问题具有以下两个特征,那么它就适合用动态规划来解决:- 最优子结构 :问题的最优解可以由子问题的最优解构成。
- 重叠子问题 :在求解过程中,存在大量的重复计算。
-
动态规划的时间复杂度是多少?
动态规划的时间复杂度通常与状态的数量和状态转移的复杂度有关。在本例中,状态的数量是n * target
,状态转移的复杂度是 O(1),所以总的时间复杂度是 O(n * target)。 -
除了分割等和子集问题,动态规划还能解决哪些问题?
动态规划可以解决很多问题,例如:- 最长公共子序列问题
- 背包问题
- 编辑距离问题
- 最短路径问题 等等。
希望这篇文章能够帮助你更好地理解动态规划,并掌握如何用它来解决实际问题。动态规划是一种非常强大的算法技术,它可以帮助我们解决很多看似复杂的问题。只要你掌握了它的核心思想,就能灵活运用它来解决各种各样的问题。