C++ 背包九讲:从入门到精通的动态规划专题

1. 引言

背包问题是动态规划领域最经典、最基础的一类问题,也是算法竞赛和面试中高频出现的考点。所谓「背包九讲」,是指围绕背包问题展开的九种经典变体与解法思路,从最简单的 01 背包,到完全背包、多重背包,再到分组背包、依赖背包等进阶模型。掌握这些模型,不仅能解决背包本身,更能帮助你建立动态规划的状态设计思维。

本文以 C++ 为语言载体,逐讲拆解九种背包问题的定义、状态转移方程、核心代码与优化技巧,并给出可直接运行的示例。

2. 第一讲:01 背包

01 背包是最基础的背包模型:有 N 件物品和一个容量为 V 的背包,每件物品只能选择放入一次(0 或 1 次),第 i 件物品的体积为 ci,价值为 wi,求在不超过背包容量的前提下能获得的最大总价值。

状态定义:dpj 表示容量为 j 的背包能装下的最大价值。核心转移方程为:

cpp 复制代码
for (int i = 1; i <= N; i++) {
    for (int j = V; j >= c[i]; j--) {
        dp[j] = max(dp[j], dp[j - c[i]] + w[i]);
    }
}

这里内层循环必须从大到小遍历,目的是保证每件物品只被使用一次,避免重复选取。

3. 第二讲:完全背包

完全背包与 01 背包的唯一区别在于:每件物品可以无限次选取。因此内层循环改为从小到大遍历,即可实现同一物品的多次累加。

cpp 复制代码
for (int i = 1; i <= N; i++) {
    for (int j = c[i]; j <= V; j++) {
        dp[j] = max(dp[j], dp[j - c[i]] + w[i]);
    }
}

两种背包代码只差一个循环方向,但语义完全不同,务必理解其背后的递推逻辑。

4. 第三讲:多重背包

多重背包中,第 i 件物品最多可选 ki 次。朴素做法是在 01 背包基础上再枚举选取数量,时间复杂度为 O(N·V·k)。更高效的做法是二进制拆分:把 ki 件物品拆成若干组,每组数量为 1、2、4、......、剩余部分,从而转化为 01 背包求解。

cpp 复制代码
for (int i = 1; i <= N; i++) {
    int num = k[i];
    for (int p = 1; p <= num; p <<= 1) {
        int cnt = min(p, num);
        for (int j = V; j >= cnt * c[i]; j--) {
            dp[j] = max(dp[j], dp[j - cnt * c[i]] + cnt * w[i]);
        }
        num -= cnt;
    }
}

5. 第四讲:混合背包

混合背包是指同一问题中同时存在 01 背包、完全背包和多重背包的物品。处理方式很直接:遍历每件物品时,根据其类型选择对应的循环方向与拆分策略即可。

cpp 复制代码
for (int i = 1; i <= N; i++) {
    if (type[i] == 0) { // 01 背包
        for (int j = V; j >= c[i]; j--)
            dp[j] = max(dp[j], dp[j - c[i]] + w[i]);
    } else if (type[i] == 1) { // 完全背包
        for (int j = c[i]; j <= V; j++)
            dp[j] = max(dp[j], dp[j - c[i]] + w[i]);
    } else { // 多重背包,二进制拆分后按 01 处理
        // 省略拆分细节
    }
}

6. 第五讲:二维费用背包

二维费用背包是指每件物品同时占用两种资源(如体积和重量),背包也有两个容量上限。此时状态扩展为二维:dpjk 表示体积不超过 j、重量不超过 k 时的最大价值。

cpp 复制代码
for (int i = 1; i <= N; i++) {
    for (int j = V; j >= c[i]; j--) {
        for (int k = M; k >= d[i]; k--) {
            dp[j][k] = max(dp[j][k], dp[j - c[i]][k - d[i]] + w[i]);
        }
    }
}

7. 第六讲:分组背包

分组背包中,物品被划分为若干组,每组内最多只能选择一件。处理时先枚举组,再枚举容量,最后枚举组内物品,保证每组至多取一个。

cpp 复制代码
for (int g = 1; g <= G; g++) {
    for (int j = V; j >= 0; j--) {
        for (int k = 0; k < group[g].size(); k++) {
            int c = group[g][k].c, w = group[g][k].w;
            if (j >= c) dp[j] = max(dp[j], dp[j - c] + w);
        }
    }
}

8. 第七讲:有依赖的背包

有依赖的背包是指某些物品必须在其「主件」被选取后才能选取,典型场景是树形结构。一般思路是:对每个主件,先对其附件做一次 01 背包,得到该主件在不同容量下的最优价值,再与主件合并,最后对所有主件做一次分组背包。

9. 第八讲:泛化物品与背包问题扩展

泛化物品是指价值随分配容量变化的物品,即 w = f(c)。这类问题可以统一用「泛化物品」的视角看待:每种背包模型本质上都是在容量与价值之间做最优分配。理解这一点,有助于把新问题抽象为已知模型。

10. 第九讲:背包问题方案总数与最优方案

除了求最大价值,背包问题还常要求统计方案总数或输出具体方案。方案总数只需把 max 改为求和:

cpp 复制代码
dp[0] = 1;
for (int i = 1; i <= N; i++) {
    for (int j = V; j >= c[i]; j--) {
        dp[j] += dp[j - c[i]];
    }
}

输出具体方案则需要在转移时记录选择路径,通常用二维数组 pathij 标记第 i 件物品在容量 j 下是否被选取,最后逆序回溯。

11. 总结与练习建议

背包九讲的核心在于理解「状态定义」与「循环顺序」背后的递推语义。建议按以下顺序练习:先吃透 01 背包与完全背包的代码差异,再掌握多重背包的二进制拆分,最后挑战分组与依赖背包。每道题都亲手写出状态转移方程,再对照代码验证,才能真正内化动态规划思想。

12. 10 道练习题(洛谷题号)

下面给出 10 道覆盖背包九讲各知识点的洛谷练习题,建议按难度循序渐进地完成:

  1. P1048 采药:01 背包入门题,适合用来熟悉状态转移方程与逆序循环。
  2. P1616 疯狂的采药:完全背包基础题,体会正序循环与 01 背包的差异。
  3. P1060 开心的金明:01 背包 + 价值加权,练习把题目条件转化为背包模型。
  4. P1049 装箱问题:01 背包变形,求剩余空间最小,训练逆向思维。
  5. P1164 小A点菜:方案总数类背包,对应第九讲内容,练习把 max 改为求和。
  6. P1776 宝物筛选:多重背包经典题,必须使用二进制拆分优化才能通过。
  7. P1833 樱花:混合背包综合题,同时包含 01、完全与多重背包,对应第四讲。
  8. P1855 榨取kkksc03:二维费用背包,对应第五讲,同时限制时间和金钱两种资源。
  9. P1757 通天之分组背包:分组背包模板题,对应第六讲,注意枚举顺序。
  10. P1064 金明的预算方案:有依赖的背包经典题,对应第七讲,主件与附件需合并处理。
相关推荐
qq_4017004142 分钟前
Qt 组播(Multicast)实战详解
开发语言·qt
Sapphire~1 小时前
odoo-095 视图的重写规则(全部重写还是部分改写)
开发语言·javascript·ecmascript
利威尔xu1 小时前
Go GMP 调度器 10 个高频追问:3 个线上故障让 P99 从 80ms 飙到 2s、10 万 goroutine 泄漏
开发语言·后端·面试·性能优化·golang
中电金信1 小时前
从业务调优到分层缓存:DeepSeek V4 Flash的PPU实践
java·开发语言
梦影_1 小时前
MySQL进阶教程(二)—— 储存过程与触发器(下)
开发语言·数据库·后端·sql·mysql·oracle·数据库开发
lg_cool_1 小时前
Python 代码打包
开发语言·python
木雷坞1 小时前
PHP两项9.2分漏洞:Docker容器更新与版本核验教程
开发语言·docker·php
2601_962885721 小时前
如何用 Python 做 MACD 金叉全市场选股?
开发语言·python
FfHUCisI1 小时前
无锁 Reactor 网络模型与 gnet / evio 架构拆解
服务器·开发语言·后端·golang