14 背包问题专题拓展

GESP C++ 六级:背包问题专题拓展

核心目标:在掌握 01 背包与完全背包的基础上,系统掌握方案数背包、分组背包、多重背包与二进制优化、混合背包、多维背包五类变形模型,建立“看到题面 → 识别模型 → 套用模板”的场景识别能力。
背包家族的区分只看三件事——每件物品能用几次、物品之间有没有分组约束、dp 数组存的是价值还是方案数;01 背包倒序,完全背包正序,分组背包最内层遍历组内物品,方案数背包把 max 改成加法。

一、背包家族全景图

模型 物品使用次数 核心转移 循环特征
01 背包 每件最多 1 次 dp[j] = max(dp[j], dp[j-w]+v) 容量倒序
完全背包 每件无限次 容量正序
多重背包 每件有固定数量 k 二进制拆分后转为 01 背包 拆分后倒序
分组背包 每组最多选 1 件 枚举组内物品,取最优 组 → 容量倒序 → 组内物品
方案数背包 同 01/完全 dp[j] += dp[j-w] 与对应背包一致
混合背包 每件类型不同 按类型分别处理 01 倒序,完全正序

场景识别口诀:“一个物品能用几次?”→ 01(1次)/ 完全(无限)/ 多重(有限次);“物品有没有分组?”→ 分组背包;“问最大价值还是方案数?”→ 价值背包用 max,方案数背包用加法。

二、方案数背包

2.1 核心思想

方案数背包不再问“最大价值是多少”,而是问“恰好装满容量 j 的方案有多少种”。核心变化只有一处:把状态转移从 max 改为加法。

终极口诀:价值背包求最优,状态转移用 max;方案背包数方法,状态转移改加法。dp[0] 最为重要,空方案要记牢!

2.2 01 背包方案数

状态:dp[j] 表示恰好装满容量 j 的方案数。

初始化:dp[0] = 1(什么都不选也是一种方案),其余 dp[j] = 0。

转移:dp[j] += dp[j-w[i]],容量倒序。

int dp[10005] = {0};
dp[0] = 1;

for (int i = 1; i <= n; i++) {
    for (int j = W; j >= w[i]; j--) {   // 倒序,每件只用一次
        dp[j] += dp[j - w[i]];
    }
}
cout << dp[W] << endl;

经典例题: 小A点菜——有 M 元,N 道菜,每道菜价格 v[i],问恰好把 M 元花完的点菜方案有多少种。这是求恰好装满方案数的模板题。

2.3 完全背包方案数

与 01 方案数背包的唯一区别:容量正序遍历。

int dp[10005] = {0};
dp[0] = 1;

for (int i = 1; i <= n; i++) {
    for (int j = w[i]; j <= W; j++) {   // 正序,物品可重复使用
        dp[j] += dp[j - w[i]];
    }
}
cout << dp[W] << endl;

经典应用:硬币问题——有 1 元、2 元、5 元硬币无限个,问组成 10 元有多少种方案。

2.4 方案数背包的初始化对比

问法 dp[0] 其余 遍历方向
01 方案数(恰好装满) 1 0 倒序
完全方案数(恰好装满) 正序
最大价值(不要求装满) 0 依背包类型
最大价值(恰好装满) -INF

关键提醒:方案数背包中 dp[0] = 1 是最容易出错的地方。很多同学会误以为“凑出重量 0 有 0 种方法”,实际上“什么都不选”就是一种方案。

三、分组背包

3.1 问题模型

物品被分成若干组,每组最多只能选一个物品(也可以一个都不选)。例如选课系统中“数学组选一门、英语组选一门、科学组选一门”。

口诀:每组最多挑一个,组内物品不能叠。

3.2 状态定义

dp[j] 表示前 i 组物品、容量为 j 时的最大价值。注意:这里的 i 表示的是前 i 组,而不是前 i 件物品——这是最容易混淆的地方。

3.3 转移方程与循环结构

对于第 i 组,组内有若干物品。对容量 j,尝试组内每一个物品,取最优:

三层循环结构:最外层枚举组,中间层倒序枚举容量,最内层枚举组内物品。

for (int i = 1; i <= g; i++) {                 // 外层:枚举组
    for (int j = W; j >= 0; j--) {             // 中间:容量倒序
        for (int k = 0; k < group[i].size(); k++) {   // 内层:组内物品
            int w = group[i][k].w, v = group[i][k].v;
            if (j >= w) {
                dp[j] = max(dp[j], dp[j - w] + v);
            }
        }
    }
}
cout << dp[W] << endl;

为什么中间层容量要倒序? 倒序保证每个物品最多被选一次(每组最多选一个),如果正序,同一组的物品可能会被重复选择,退化为完全背包。

3.4 一维优化写法

int dp[10005] = {0};
for (int i = 1; i <= g; i++) {
    for (int j = W; j >= 0; j--) {
        for (auto &item : group[i]) {
            if (j >= item.w) {
                dp[j] = max(dp[j], dp[j - item.w] + item.v);
            }
        }
    }
}

注意:分组背包的“每组最多选一个”约束,使得内层遍历组内物品时不能直接套用 01 背包的模板。必须保证同一组的物品不会在同一轮容量循环中被重复选中,因此容量倒序是必需的。

3.5 真题对接:学习小组

将 n 名学生分成若干个连续编号的小组,每个小组人数为 i 时有对应的奖励值 a[i]。这道题容易被误判为 01 背包,但题目并未限制小组人数不能重复,因此同一人数的小组可以出现多次——这是完全背包,而不是分组背包。

四、多重背包与二进制优化

4.1 问题模型

每件物品有固定数量 k 个,可以选择 0 到 k 个。例如“宝剑有 3 把”。

4.2 朴素解法

把 k 个相同物品看作 k 件独立的 01 背包物品,逐一处理。复杂度 O(n × W × k),当 k 较大时会超时。

4.3 二进制拆分优化(核心)

核心思想:把 k 件相同物品拆分成若干“捆”,每捆的数量是 1、2、4、8……这样的 2 的幂次。

为什么是 2 的幂:任何正整数都可以表示为若干个 2 的幂之和。例如 13 = 1 + 4 + 8(或 1 + 2 + 4 + 6)。通过这种方式,可以用 O(log k) 个“捆绑物品”表示 0~k 的任意数量选择。

拆分规则:设剩余数量为 rem,当前拆分大小为 p = 1:

int rem = k, p = 1;
while (rem > 0) {
    int take = min(p, rem);
    // 将 take 个物品打包成一个新物品
    w_new = take * w[i];
    v_new = take * v[i];
    // 这个新物品按 01 背包处理
    rem -= take;
    p *= 2;
}

举例:k = 13,拆分过程:1, 2, 4, 6(剩余 13-1-2-4=6,最后一块直接取剩余)。

拆分后,对每个“捆绑物品”执行 01 背包的容量倒序遍历即可。

4.4 完整代码模板

int dp[10005] = {0};
for (int i = 1; i <= n; i++) {
    int k = cnt[i];              // 第 i 种物品的数量
    int p = 1;
    while (k > 0) {
        int take = min(p, k);
        int w_new = take * w[i];
        int v_new = take * v[i];
        // 01 背包处理
        for (int j = W; j >= w_new; j--) {
            dp[j] = max(dp[j], dp[j - w_new] + v_new);
        }
        k -= take;
        p *= 2;
    }
}
cout << dp[W] << endl;

复杂度:O(n × W × log k),远优于朴素解法。

4.5 真题对接:道具商店(GESP202512 六级 )

这道题涉及用金币提升攻击力,物品有数量限制,需要多重背包或二进制优化处理。

五、混合背包

5.1 问题模型

一个背包问题中同时出现多种类型的物品——有的只能选一次(01),有的可以无限选(完全),有的有固定数量(多重)。

5.2 核心思想

混合背包没有新的 DP 模型。它的本质是:按物品类型分别处理,每种物品套用对应背包的循环模板即可。

输入格式识别:题目通常用 s 表示物品类型:

  • s = -1:01 背包,只能选一次
  • s = 0:完全背包,无限选
  • s > 0:多重背包,有 s 个

5.3 统一处理模板

for (int i = 1; i <= n; i++) {
    if (type[i] == 1) {                     // 01 背包
        for (int j = W; j >= w[i]; j--) {
            dp[j] = max(dp[j], dp[j-w[i]] + v[i]);
        }
    } else if (type[i] == 0) {              // 完全背包
        for (int j = w[i]; j <= W; j++) {
            dp[j] = max(dp[j], dp[j-w[i]] + v[i]);
        }
    } else {                                 // 多重背包
        int k = cnt[i], p = 1;
        while (k > 0) {
            int take = min(p, k);
            int w_new = take * w[i], v_new = take * v[i];
            for (int j = W; j >= w_new; j--) {
                dp[j] = max(dp[j], dp[j-w_new] + v_new);
            }
            k -= take;
            p *= 2;
        }
    }
}

5.4 真题对接:小杨买饮料(GESP 六级)

涉及多种类型的物品混合,需要根据每件物品的类型选择对应的循环方向。

六、多维背包(了解)

6.1 问题模型

每个物品不仅消耗一种资源(如体积),还消耗另一种资源(如重量),背包对两种资源都有上限。

6.2 核心思路

状态数组升维:dp[j][k] 表示第一种资源为 j、第二种资源为 k 时的最大收益。转移时两层容量都倒序(01 背包)。

for (int i = 1; i <= n; i++) {
    for (int j = W1; j >= w1[i]; j--) {
        for (int k = W2; k >= w2[i]; k--) {
            dp[j][k] = max(dp[j][k], dp[j-w1[i]][k-w2[i]] + v[i]);
        }
    }
}

六级对多维背包的考查较少,了解状态升维的思路即可。

七、场景识别速查表

看到一道背包题时,按以下顺序判断:

判断问题 答案 模型
问最大价值还是方案数? 方案数 方案数背包(转移改加法)
每件物品能用几次? 1 次 01 背包(容量倒序)
无限次 完全背包(容量正序)
固定 k 次 多重背包(二进制拆分→01)
物品有分组吗? 每组最多选一个 分组背包(组→容量倒序→组内物品)
多种类型混合? 01+完全+多重 混合背包(按类型分别处理)
消耗多种资源? 体积+重量等 多维背包(状态升维)

终极口诀:“一个物品能用几次?”——这是看到背包题时第一个要问自己的问题。

八、易错点汇总

  1. 方案数背包 dp[0] 初始化为 0:应该为 1(空方案是一种方案)。
  2. 分组背包容量正序遍历:会导致同组物品被重复选择,必须倒序。
  3. 多重背包不拆分直接当 01 背包:k 较大时超时,必须二进制拆分。
  4. 二进制拆分最后一块忘记取 min:take = min(p, rem),不是直接取 p。
  5. 混合背包中完全背包用倒序:完全背包必须正序,倒序会退化为 01 背包。
  6. 多维背包容量循环方向:01 多维背包两层容量都倒序,完全多维背包都正序。
  7. 方案数背包用 max 而非加法:方案数的转移是 +=,不是 max。
  8. 方案数溢出:方案数可能很大,通常需要对 10^9+7 取模。
  9. 分组背包状态定义混淆:dp[i][j] 中的 i 是“前 i 组”,不是“前 i 件物品”。
  10. 混合背包类型识别错误:s = -1 是 01,s = 0 是完全,s > 0 是多重。

九、总结

背包问题专题拓展是 GESP 六级动态规划模块的“最后一公里”。

  1. 背包家族的区分只看三件事:物品能用几次、有没有分组、dp 存价值还是方案数。
  2. 方案数背包的核心变化:把 max 改为 +=,dp[0] = 1。
  3. 01 方案数倒序,完全方案数正序,与价值背包的循环方向完全一致。
  4. 分组背包:每组最多选一个,三层循环“组 → 容量倒序 → 组内物品”。
  5. 多重背包:二进制拆分把 k 件物品拆成 O(log k) 个捆绑物品,再按 01 背包处理。
  6. 混合背包没有新模型,按物品类型分别套用 01 / 完全 / 多重模板。
  7. 多维背包:状态升维,dp[j][k] 表示两种资源分别为 j 和 k。
  8. 看到背包题先问“一个物品能用几次”,这是识别模型的第一把钥匙。
  9. 分组背包的 i 是“前 i 组”,不是“前 i 件物品”,这是最容易混淆的地方。
  10. 多练“小A点菜(方案数)”“学习小组(完全)”“道具商店(多重)”“小杨买饮料(混合)”四类题。

复习建议:先把 01 背包和完全背包的模板写熟,确保闭卷能默写;再重点攻克方案数背包,牢记 dp[0]=1 和加法转移;然后练习分组背包的三层循环和多重背包的二进制拆分;最后用“道具商店”“小杨买饮料”等真题做场景识别训练,看到题面能快速判断属于哪一类背包。考试中先判断“物品能用几次”,再判断“有没有分组”,最后判断“求价值还是方案数”,三步锁定模型后套用对应模板。