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值

为什么这个区别这么重要?

💡 一维数组下的本质区别

在一维数组中,区别体现在内层循环方向:

  • 逆序(01背包):计算 dp[5] 时,dp[3] 还是旧值 → 不能重复选
  • 正序(完全背包):计算 dp[5] 时,dp[3] 已经被当前物品更新过 → 可以重复选

正序遍历时,dp[w-weight[i]] 可能已经包含了选过当前物品的价值,所以相当于"同一个物品可以被选多次"。

🔢 手动推演

假设2个物品,背包容量5:

完全背包下,物品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级(相当于无限次使用两种"物品")

🚨 易错点

⚠️ 常见错误汇总
  1. 正序写成逆序:完全背包内层必须正序(从小到大),逆序就变成01背包了!
  2. 和01背包搞混:做题时先看清楚是"每个物品只能用一次"还是"可以无限使用",这决定了用哪种背包。
  3. 内层循环起始值:正序时从 weight[i] 开始(不是从1),因为容量小于物品重量时无法选择。
  4. 转移方程下标:完全背包用 dp[i][w-w[i]],不是 dp[i-1][w-w[i]]。如果用二维写法,这个下标区别很关键。

🎯 练习建议

📝 循序渐进练习路径
  1. 对比练习:拿同一个题目,分别用01背包和完全背包的代码跑一遍,观察答案差异。
  2. 理解正序的本质:在纸上推演正序和逆序时,dp数组的变化过程,理解为什么正序允许重复选择。
  3. 做经典题目:
    • P2294 [HNOI2005] 狡猾的商人(完全背包变形)
    • 零钱兑换 II(LeetCode 518)
    • 爬楼梯(LeetCode 70,本质是完全背包)
  4. 变形练习:完全背包的"最小值版本"(求最少物品数)也是常见考法。