GESP 7级
完全背包问题
7级 · DP/图论/搜索
📖 一句话理解
和01背包类似,但有一个关键区别:每个物品可以选无限次(不是只能选一次)。比如你有无限个"物品1"可以用,只要背包装得下。
💡 为什么重要?
完全背包是在01背包基础上的自然延伸,但代码变化只有一行(内层循环方向),很多同学因此搞混。掌握它的关键在于理解"为什么正序和逆序的区别这么大"。考试经常同时考01背包和完全背包,对比出题。
📋 前置知识
- 必须先学完 01背包,理解二维DP和一维空间优化
- 理解为什么01背包内层要逆序遍历
- 能区分"每个物品只能选一次"和"每个物品可以选多次"这两种场景
🔄 01背包 vs 完全背包:对比理解
01背包
每个物品只能选一次
内层循环:逆序(从W到weight[i])
比如:限量商品、考试选题
完全背包
每个物品可以选无限次
内层循环:正序(从weight[i]到W)
比如:零钱兑换、无限材料购买
📐 完全背包的DP思路
状态定义(和01背包一样)
dp[w] = 背包容量为 w 时,能获得的最大价值。
转移方程(唯一变化!)
📐 核心转移方程
dp[i][w] = max(dp[i-1][w], dp[i][w-w[i]] + v[i])
注意看下标:选第i个物品时,用的是 dp[i][...] 而不是 dp[i-1][...]
含义:当前物品可以继续选,所以用"当前轮"的dp值
注意看下标:选第i个物品时,用的是 dp[i][...] 而不是 dp[i-1][...]
含义:当前物品可以继续选,所以用"当前轮"的dp值
为什么这个区别这么重要?
💡 一维数组下的本质区别
在一维数组中,区别体现在内层循环方向:
- 逆序(01背包):计算 dp[5] 时,dp[3] 还是旧值 → 不能重复选
- 正序(完全背包):计算 dp[5] 时,dp[3] 已经被当前物品更新过 → 可以重复选
正序遍历时,dp[w-weight[i]] 可能已经包含了选过当前物品的价值,所以相当于"同一个物品可以被选多次"。
🔢 手动推演
假设2个物品,背包容量5:
- 物品1:重量2,价值3
- 物品2:重量3,价值4
完全背包下,物品1可以选多次!
📊 dp数组推演(正序遍历)
考虑物品1(重量2,价值3),正序遍历:
dp[2] = max(dp[2], dp[0]+3) = 3 ✓(选1个物品1)
dp[4] = max(dp[4], dp[2]+3) = 6 ✓(选2个物品1,重量4,价值6)
考虑物品2(重量3,价值4),正序遍历:
dp[3] = max(dp[3], dp[0]+4) = 4 ✓
dp[5] = max(dp[5], dp[2]+4) = max(?, 3+4) = 7 ✓
最终答案:dp[5] = 7(选1个物品1+1个物品2,总重5,总价值7)
💻 完整代码
💻 完全背包(一维优化版)
#include <iostream>
#include <algorithm> // max()函数
using namespace std;
int main() {
int n = 2; // 物品数量
int W = 5; // 背包最大容量
int weight[] = {2, 3}; // 每个物品的重量
int value[] = {3, 4}; // 每个物品的价值
int dp[6] = {0}; // dp[w] = 容量为w时的最大价值,初始为0
for (int i = 0; i < n; i++) { // 遍历每个物品
for (int w = weight[i]; w <= W; w++) { // ⚠️ 正序!从小到大!
// 和01背包唯一的区别就是这里!
// dp[w]:不选当前物品
// dp[w-weight[i]] + value[i]:选当前物品(可能选多次)
dp[w] = max(dp[w], dp[w - weight[i]] + value[i]);
}
}
cout << "最大价值: " << dp[W] << endl; // 输出答案
return 0;
}📋 01背包 vs 完全背包 代码对比
💻 两者核心代码对比(只有循环方向不同!)
// 01背包:内层逆序(从大到小)
for (int i = 0; i < n; i++)
for (int w = W; w >= weight[i]; w--) // ← 逆序!
dp[w] = max(dp[w], dp[w-weight[i]] + value[i]);
// 完全背包:内层正序(从小到大)
for (int i = 0; i < n; i++)
for (int w = weight[i]; w <= W; w++) // ← 正序!
dp[w] = max(dp[w], dp[w-weight[i]] + value[i]);
// ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
// 除此之外,代码完全一样!🎯 经典应用场景
💡 完全背包的实际应用
- 零钱兑换:给你不同面值的硬币(无限个),凑出目标金额的最少硬币数
- 无限材料购买:商店里每种商品无限供应,预算固定,买什么最划算
- 爬楼梯:每次可以走1级或2级(相当于无限次使用两种"物品")
🚨 易错点
⚠️ 常见错误汇总
- 正序写成逆序:完全背包内层必须正序(从小到大),逆序就变成01背包了!
- 和01背包搞混:做题时先看清楚是"每个物品只能用一次"还是"可以无限使用",这决定了用哪种背包。
- 内层循环起始值:正序时从 weight[i] 开始(不是从1),因为容量小于物品重量时无法选择。
- 转移方程下标:完全背包用 dp[i][w-w[i]],不是 dp[i-1][w-w[i]]。如果用二维写法,这个下标区别很关键。
🎯 练习建议
📝 循序渐进练习路径
- 对比练习:拿同一个题目,分别用01背包和完全背包的代码跑一遍,观察答案差异。
- 理解正序的本质:在纸上推演正序和逆序时,dp数组的变化过程,理解为什么正序允许重复选择。
- 做经典题目:
- P2294 [HNOI2005] 狡猾的商人(完全背包变形)
- 零钱兑换 II(LeetCode 518)
- 爬楼梯(LeetCode 70,本质是完全背包)
- 变形练习:完全背包的"最小值版本"(求最少物品数)也是常见考法。