资讯动态

动态规划实战:力扣416与1049题01背包解法

发布时间:2026/9/10 11:38:46 来源:尧图企业网站定制
1. 动态规划实战力扣416与1049题精解作为算法工程师动态规划DP是必须掌握的硬核技能。今天我想分享两道经典的力扣DP题目——第416题「分割等和子集」和第1049题「最后一块石头的重量 II」。这两题看似不同实则都暗藏01背包问题的精髓。我在面试候选人和实际工作中发现能灵活运用DP思想解决问题的开发者往往具备更强的系统设计能力。2. 题目背景与核心思路2.1 力扣416题分割等和子集给定一个只包含正整数的非空数组判断是否可以将这个数组分割成两个子集使得两个子集的元素和相等。例如输入[1,5,11,5]可以分割成[1,5,5]和[11]因此返回true。这题的关键在于发现如果能找到若干元素的和等于总和的一半就满足题意。这本质上是在数组中寻找一个子序列使其和为特定值——这正是01背包问题的变种。2.2 力扣1049题最后一块石头的重量 II有一堆石头每块石头的重量都是正整数。每次任意选两块石头碰撞粉碎后剩下重量差。重复直到只剩一块求最小的可能重量。例如[2,7,4,1,8,1]最优解是1。这道题的突破点在于将石头分成两堆使两堆重量差最小。这又回到了子集和问题——尽可能让两堆石头重量接近总和的一半。3. 动态规划解法详解3.1 01背包问题回顾01背包问题的经典形式是给定物品重量w和价值v在容量为W的背包中如何选择物品使总价值最大。其DP状态定义为dp[i][j] 前i个物品中总重量不超过j时的最大价值 状态转移方程 dp[i][j] max(dp[i-1][j], dp[i-1][j-w[i]] v[i])在实际编码中我们通常使用一维数组优化空间复杂度dp [0] * (W 1) for i in range(n): for j in range(W, w[i] - 1, -1): dp[j] max(dp[j], dp[j - w[i]] v[i])3.2 416题的DP实现对于416题我们可以将问题转化为是否存在子集其和为sum(nums)//2。DP状态定义dp[i][j] 前i个数中是否能选出若干数使和为jPython实现def canPartition(nums): total sum(nums) if total % 2 ! 0: return False target total // 2 dp [False] * (target 1) dp[0] True for num in nums: for j in range(target, num - 1, -1): dp[j] dp[j] or dp[j - num] return dp[target]注意这里的内层循环需要从大到小遍历避免重复计算。这是01背包问题的经典优化技巧。3.3 1049题的DP解法1049题可以转化为将石头分成两堆使两堆重量差最小。DP状态与416题类似def lastStoneWeightII(stones): total sum(stones) target total // 2 dp [False] * (target 1) dp[0] True for stone in stones: for j in range(target, stone - 1, -1): dp[j] dp[j] or dp[j - stone] max_weight max([i for i in range(target 1) if dp[i]]) return total - 2 * max_weight这个解法巧妙地将问题转化为寻找最接近total//2的子集和最终结果就是总重量减去两倍的该子集和。4. 算法优化与边界处理4.1 空间优化技巧在上述实现中我们使用了滚动数组的技巧将空间复杂度从O(nW)优化到O(W)。这是通过反向遍历实现的for j in range(target, num - 1, -1): dp[j] dp[j] or dp[j - num]如果正向遍历会导致一个物品被多次使用这就变成了完全背包问题。反向遍历保证了每个物品只被考虑一次。4.2 提前终止条件在416题中我们可以添加一些优化如果总和为奇数直接返回False如果最大元素超过总和一半直接返回False在DP过程中一旦发现target可达可以立即返回优化后的代码def canPartition(nums): total sum(nums) if total % 2 ! 0: return False target total // 2 nums.sort(reverseTrue) if nums[0] target: return False dp [False] * (target 1) dp[0] True for num in nums: for j in range(target, num - 1, -1): if dp[target]: return True dp[j] dp[j] or dp[j - num] return dp[target]4.3 1049题的特殊情况处理对于1049题需要考虑以下边界情况空数组返回0单元素数组返回该元素所有元素相同如果数量为偶数返回0奇数返回该元素值实际编码中可以这样处理if not stones: return 0 if len(stones) 1: return stones[0]5. 常见错误与调试技巧5.1 初始化错误常见错误是忘记初始化dp[0]True。这会导致整个DP过程失效因为任何子集和都需要从空集开始构建。5.2 遍历顺序错误另一个常见错误是内层循环采用正向遍历for j in range(num, target 1): # 错误这会变成完全背包 dp[j] dp[j] or dp[j - num]这会使得每个物品被多次使用导致结果错误。5.3 数值溢出问题当数组元素很大或很多时总和可能超过普通整型范围。在Python中这不是问题但在其他语言如C中需要注意// 在C中可能需要使用long long类型 vectorlong long dp(target 1, 0);5.4 调试技巧当DP结果不符合预期时可以打印DP表的中间状态检查初始条件是否正确验证状态转移方程是否与思路一致使用小规模测试用例手动计算对比例如在416题中可以添加调试输出print(fProcessing num{num}) for j in range(target, -1, -1): if dp[j]: print(fCan make sum {j})6. 复杂度分析与变种题目6.1 时间复杂度分析两题的时间复杂度均为O(nW)其中n是数组长度W是目标和416题中为sum//2。空间复杂度为O(W)。在实际应用中当W很大时如超过1e6这种解法可能不够高效。此时可以考虑其他方法如位运算优化适用于元素范围较小的情况折半搜索将数组分成两半分别计算可能的子集和近似算法如果不要求精确解6.2 相关变种题目掌握了这两题后可以尝试以下变种力扣494题「目标和」给数组中的数添加正负号使和为target力扣474题「一和零」二维费用的背包问题力扣322题「零钱兑换」完全背包问题力扣518题「零钱兑换II」背包问题的组合数这些题目都是背包问题的变种核心思想相通但各有特点。7. 实际应用场景动态规划特别是背包问题在实际中有广泛应用资源分配问题如服务器资源分配、投资组合优化生产计划如原材料切割、排产计划计算机视觉如图像分割、目标检测中的优化问题自然语言处理如序列标注、文本生成中的解码算法以416题为例类似的思想可以用于负载均衡将任务分配到两台服务器使负载尽可能均衡数据分片将数据分成两部分使每部分的大小相近财务规划将资产分成两部分使每部分的价值相等8. 个人经验分享在解决这类问题时我总结了一些实用技巧先写出暴力解法再思考如何用DP优化画DP表帮助理解状态转移从小规模例子入手手动计算验证思路注意DP数组的初始化条件和边界情况空间优化时务必小心遍历顺序对于面试准备建议理解经典DP问题的核心思想掌握几种常见的DP模式背包、LIS、LCS等多练习变种题目培养问题转化能力注意代码的简洁性和可读性最后分享一个调试技巧当DP结果不符合预期时可以尝试用更小的测试用例逐步打印DP表的状态这样更容易发现哪里出了问题。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价