文章目录
背包问题是动态规划(DP)中的经典应用场景,刷 LeetCode 中的背包类问题时,往往会遇到多种变体。要高效解决这类问题,需要系统化的方法和解题步骤。以下是一套通用的 背包问题解题方法论,帮助你应对LeetCode中的背包题目。
一、背包问题的分类与核心思路
在LeetCode中,背包问题可以大致分为以下几类:
- 0-1 背包问题:每个物品只能选择一次。
- 完全背包问题:每个物品可以选择无限次。
- 多重背包问题:每个物品有固定的数量可以选择。
- 分组背包问题:物品分组,每组只能选择一个物品。
- 混合背包问题:可能同时包含0-1背包、完全背包、和多重背包的情况。
这些问题的核心都是 在给定条件下优化某个指标(通常是最大/最小价值或方案数),即如何通过决策使得物品组合的总价值或解法方案在不超过背包容量的情况下达到最优。
二、通用解题步骤
1. 明确问题类型
在拿到一个背包类问题时,首先要弄清楚属于哪一类背包问题:
- 物品是可以选一次还是无限次?
- 问题要求是最大价值、最小代价,还是方案数?
明确这些后,可以决定使用哪种动态规划策略。
2. 状态定义(dp数组的含义)
- 一维dp数组:
dp[j]
通常表示在背包容量为j
时,能够获得的最大(或最小)价值,或解决该问题的方案数。 - 二维dp数组:二维数组
dp[i][j]
,表示前i
个物品在背包容量为j
时的最优解。 - 二维 dp 优化为一维 dp:通常背包问题可以通过滚动数组优化空间复杂度。只需一个一维数组保存状态,减少空间占用。
3. 状态转移方程
状态转移是背包问题的核心。关键是如何通过已经计算好的状态,推导出新状态。通常有以下几种思路:
- 0-1 背包 :每个物品只能取一次。如果选择第
i
个物品,那么容量减少weight[i]
,价值增加value[i]
。状态转移方程为:dp[j] = max(dp[j], dp[j - weight[i]] + value[i])
- 完全背包:与0-1背包的状态转移方程一样:
dp[j] = max(dp[j], dp[j - weight[i]] + value[i])
4. 初始化逻辑
- dp数组的初始化:一般情况下,
dp[0]
设为0,表示容量为0时,背包的最大价值为0。其他dp[j]
初始化为极小值或极大值,具体取决于问题要求(最大价值问题设为0,最小值问题设为无穷大)。 - 方案数问题:如果要求解不同的方案数,则
dp[0]
通常设为1,表示容量为0时有1种方式(即什么都不装),求解不同的方案数主要是通过累加来转移状态,如果初始化是0,那后面就都是0。
5. 遍历顺序
背包问题中,遍历顺序影响状态转移过程:
- 0-1 背包:需要倒序遍历背包容量,防止同一物品被多次选取。
for i := 0; i < n; i++ { for j := capacity; j >= weight[i]; j-- { dp[j] = max(dp[j], dp[j - weight[i]] + value[i]) } }
- 完全背包:正序遍历容量,允许物品被多次选择。
for i := 0; i < n; i++ { for j := weight[i]; j <= capacity; j++ { dp[j] = max(dp[j], dp[j - weight[i]] + value[i]) } }
三、背包问题常见变体的解题策略
1. 0-1 背包问题
每个物品只能选择一次,目标是最大化背包内物品的总价值。关键是倒序遍历背包容量,避免重复选择物品。
for i := 0; i < n; i++ {
for j := capacity; j >= weight[i]; j-- {
dp[j] = max(dp[j], dp[j - weight[i]] + value[i])
}
}
2. 完全背包问题
每个物品可以被选择无限次,解决方案类似于0-1背包,但需要正序遍历容量。
for i := 0; i < n; i++ {
for j := weight[i]; j <= capacity; j++ {
dp[j] = max(dp[j], dp[j - weight[i]] + value[i])
}
}
3. 背包问题求方案数
如果问题要求求出装满背包的不同方法数,通常转移方程需要使用累加:
dp[j] += dp[j - weight[i]]
此时对dp数组的初始化也不一样,dp[0]=1。
4. 最小个数问题
例如,求装满背包所需的最少物品数。此时,状态转移方程为:
dp[j] = min(dp[j], dp[j - weight[i]] + 1)
四、完整实例分析
以下是一个 0-1 背包问题的具体解法:
func knapsack(weights []int, values []int, capacity int) int {
dp := make([]int, capacity+1)
for i := 0; i < len(weights); i++ {
for j := capacity; j >= weights[i]; j-- {
dp[j] = max(dp[j], dp[j - weights[i]] + values[i])
}
}
return dp[capacity]
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
解析:
dp[j]
代表背包容量为j
时所能获得的最大价值。- 外层循环遍历所有物品,内层循环倒序遍历背包容量,保证每个物品只使用一次。
- 使用
max()
函数来判断是选择当前物品带来的价值更大,还是不选择该物品更大。
五、常见背包问题的总结与技巧
在刷 LeetCode 背包问题时,通常会遇到不同的变体。以下是不同问题的常见模式以及应对技巧:
1. 0-1 背包问题
- 问题特征:每个物品只能选一次,要求最大化价值或最小化花费。
- 解法要点:倒序遍历容量,避免重复选取。
- 典型题目:
2. 完全背包问题
- 问题特征:每个物品可以选无限次,求解最大价值或最小个数。
- 解法要点:正序遍历容量,允许多次选择同一物品。
- 典型题目:
3. 背包问题中的组合数与排列数
- 组合数问题:背包容量为定值,问装满背包的不同组合数。通常采用正序遍历:
dp[j] += dp[j - weight[i]]
- 排列数问题:求出所有排列方式,遍历顺序不同,先遍历容量,再遍历物品:
for j := 0; j <= capacity; j++ { for i := 0; i < len(weights); i++ { dp[j] += dp[j - weight[i]] } }
- 典型题目:
4. 求最小值问题
- 问题特征:有时题目要求找到使背包正好装满时所需的最少物品数量。
- 解法要点:转移方程使用
min()
来比较并取最小值:dp[j] = min(dp[j], dp[j - weight[i]] + 1)
- 典型题目:
六、优化与高级技巧
1. 空间优化
背包问题中的很多解法可以通过 一维数组优化空间复杂度。例如,将二维的 dp[i][j]
优化为一维的 dp[j]
,只需存储当前物品和容量状态,节省空间。
2. 二进制优化多重背包
在多重背包问题中,如果一个物品只能选择有限次,可以将问题分解为多个0-1背包问题(利用二进制拆分),从而降低复杂度。
3. 注意边界条件
- 如果题目要求背包正好装满,通常需要初始化时将
dp[j]
设置为较小的值(例如负无穷),以确保不会错误计算。 - 有时题目要求输出方式或者最优解不能超过特定值,需要特别注意
dp
的初始值以及边界条件。
七、刷题练习与总结
通过上述方法论,你可以应对大多数 LeetCode 背包类问题。为了进一步巩固,建议从以下几个步骤进行刷题练习:
- 从基础题入手:先做经典的 0-1 背包、完全背包等基础题,熟悉状态定义和转移方程的推导。
- 进阶题目:接触多重背包、分组背包等变体,训练自己根据不同类型问题选择合适的遍历顺序和优化策略。
- 挑战题目:在掌握基础和进阶题后,尝试做难度更高的题目,灵活运用空间优化、二进制拆分等技巧。
总结
背包问题看似复杂,但只要掌握了其核心方法论,刷题时就能化繁为简。背包问题的解题流程包括明确问题类型、定义状态、推导转移方程、设计遍历顺序,以及进行必要的优化。刷题过程中,多从题目类型入手,总结常见模式,逐步提升解题效率。
参考资料:
如果你觉得这篇文章对你有帮助,请点赞、收藏并分享给更多的人看到,让我们一起进步!