背包问题
背包类问题是动态规划的一类问题模型,这类模型应用广泛。背包类问题通常可以转化成以下模型:有若干个物品,每个物品有自己的重量和价值。选择物品放进一个容量有限的背包里,求出在容量不超过最大限度的情况下能拿到的最大总价值。
01 背包问题
背包类问题中最简单的是 01 背包问题:有
看到这个问题很多人的第一反应是使用贪心策略。对于每个物品,计算其性价比:用这个物品的价值除以其重量,得到一个比值。性价比越高,说明该物品越划算,应该尽量拿该物品。将所有物品按照性价比从高到低排序,只要当前背包还能装得下,就按照顺序一个一个地放进背包。
贪心策略对于大多数情况是比较有效的。不过很容易找到反例,例如,背包容量是
所以可以看到,贪心策略无效,需要寻找一个动态规划的解决方案。首先划分阶段,那么应该一个一个物品地考虑,先考虑第一个物品时的决策,然后考虑新加入一个物品,决策有没有变化。而对于同一个物品,背包容量不同,最优结果也是不同的,所以背包容量也是状态的一部分。
定义
例如假设有
接下俩考虑新引入第
考虑第
最终答案就是
通过这样的方式,就完成了 01 背包问题的求解,时间复杂度为
例题:P1048 [NOIP2005 普及组] 采药
本题是 01 背包问题的模板题,采药的总时间
#include <cstdio> #include <algorithm> using std::max; const int M = 105; const int T = 1005; int dp[M][T]; int main() { int t, m; scanf("%d%d", &t, &m); for (int i = 1; i <= m; i++) { // 第i株草药 int tm, val; scanf("%d%d", &tm, &val); // 草药的采摘时间和价值 for (int j = 0; j <= t; j++) { // 背包容量 dp[i][j] = dp[i - 1][j]; // 第i株草药不选的决策必然能做 if (j >= tm) { // 背包容量足够才能考虑采摘 dp[i][j] = max(dp[i][j], dp[i - 1][j - tm] + val); } } } printf("%d\n", dp[m][t]); return 0; }
上面的代码中使用了二维数组,空间复杂度为
由于数组 dp
只有相邻两行之间有关系,可以滚动数组优化。
参考代码
#include <cstdio> #include <algorithm> using std::max; const int M = 105; const int T = 1005; int dp[2][T], tm[M], val[M]; int main() { int t, m; scanf("%d%d", &t, &m); for (int i = 1; i <= m; i++) scanf("%d%d", &tm[i], &val[i]); for (int i = 1; i <= m; i++) { int cur = i % 2, pre = 1 - cur; for (int j = 0; j <= t; j++) { dp[cur][j] = dp[pre][j]; if (j >= tm[i]) dp[cur][j] = max(dp[cur][j], dp[pre][j - tm[i]] + val[i]); } } printf("%d\n", dp[m % 2][t]); return 0; }
更进一步,能否优化到一维?可以,但要注意枚举顺序。
根据状态转移方程,要计算 D 的值,需要用到第
这时就产生了问题。当要计算位置 A 的值时,本来要用位置 C 的值,但是该值已被计算出来的位置 D 的值覆盖,拿不到想要的值,计算会发生错误。
所以需要将对
这样压缩空间后,代码变得更为简洁:
#include <cstdio> #include <algorithm> using std::max; const int M = 105; const int T = 1005; int dp[T], tm[M], val[M]; int main() { int t, m; scanf("%d%d", &t, &m); for (int i = 1; i <= m; i++) scanf("%d%d", &tm[i], &val[i]); for (int i = 1; i <= m; i++) { for (int j = t; j >= tm[i]; j--) { dp[j] = max(dp[j], dp[j - tm[i]] + val[i]); } } printf("%d\n", dp[t]); return 0; }
可以发现,最开始的代码里有一个 if
判断,而最终空间压缩后的代码中没有。优化前的代码中的 if
判断,是为了判断当前的消耗时间是否足够采摘第
例题:P1049 [NOIP2001 普及组] 装箱问题
首先,容易看出题目中的每个物品就是背包类问题中的“物品”,每个物品只能选一次,所以此题属于 01 背包问题。不过题目中并没有给出物品的价值和重量,只是给了物品的体积。需要找到对应物品价值和重量的定义方式,来吧问题转化成标准的背包问题。
题中问如何能让剩余空间最小,转化一下,其实就是问如何能尽量装最多的物品。求出物品的最大占用空间,用总空间减去最大占用空间,就能得到最小剩余空间了。所以,优化的目标就是如何利用最多的空间,可以看出,空间其实就是价值,想让价值尽可能大。同样,因为总体积不能超过
定义
参考代码
#include <cstdio> #include <algorithm> using std::max; const int N = 35; const int V = 20005; int dp[N][V], vol[N]; int main() { int v, n; scanf("%d%d", &v, &n); for (int i = 1; i <= n; i++) scanf("%d", &vol[i]); for (int i = 1; i <= n; i++) { for (int j = 0; j <= v; j++) { dp[i][j] = dp[i - 1][j]; if (j >= vol[i]) dp[i][j] = max(dp[i][j], dp[i - 1][j - vol[i]] + vol[i]); } } printf("%d\n", v - dp[n][v]); return 0; }
同样,也可以压缩到一维数组。
参考代码
#include <cstdio> #include <algorithm> using std::max; const int N = 35; const int V = 20005; int dp[V], vol[N]; int main() { int v, n; scanf("%d%d", &v, &n); for (int i = 1; i <= n; i++) scanf("%d", &vol[i]); for (int i = 1; i <= n; i++) { for (int j = v; j >= vol[i]; j--) { dp[j] = max(dp[j], dp[j - vol[i]] + vol[i]); } } printf("%d\n", v - dp[v]); return 0; }
例题:P1060 [NOIP2006 普及组] 开心的金明
此题是标准的 01 背包模板题。价格与重要度的乘积是该物品的价值,所以在读入重要度
参考代码
#include <cstdio> #include <algorithm> using std::max; const int M = 30; const int N = 30005; int v[M], w[M], dp[M][N]; int main() { int n, m; scanf("%d%d", &n, &m); for (int i = 1; i <= m; i++) { scanf("%d%d", &v[i], &w[i]); w[i] *= v[i]; } for (int i = 1; i <= m; i++) { // 第i个物品 for (int j = 0; j <= n; j++) { // 钱数 dp[i][j] = dp[i - 1][j]; // 不要这个物品或钱不够 if (j >= v[i]) dp[i][j] = max(dp[i][j], dp[i - 1][j - v[i]] + w[i]); // 钱够 } } printf("%d\n", dp[m][n]); return 0; }
同样,也可以压缩到一维数组。
参考代码
#include <cstdio> #include <algorithm> using std::max; const int M = 30; const int N = 30005; int v[M], w[M], dp[N]; int main() { int n, m; scanf("%d%d", &n, &m); for (int i = 1; i <= m; i++) { scanf("%d%d", &v[i], &w[i]); w[i] *= v[i]; } for (int i = 1; i <= m; i++) { for (int j = n; j >= v[i]; j--) { dp[j] = max(dp[j], dp[j - v[i]] + w[i]); } } printf("%d\n", dp[n]); return 0; }
例题:P1164 小A点菜
由于“每种菜品只有一份”,所以本题是 01 背包问题。与前面不同的是,这里要求的不是最大价值而是方案数。
在 01 背包问题中,我们是针对顶
注意,当容量为
参考代码
#include <cstdio> const int N = 105; const int M = 10005; int dp[N][M]; int main() { int n, m; scanf("%d%d", &n, &m); dp[0][0] = 1; for (int i = 1; i <= n; i++) { int a; scanf("%d", &a); for (int j = 0; j <= m; j++) { dp[i][j] = dp[i - 1][j]; if (j >= a) dp[i][j] += dp[i - 1][j - a]; } } printf("%d\n", dp[n][m]); return 0; }
例题:P1510 精卫填海
根据题意,剩下的
此题想要尽可能留更多的体力,也就是尽可能少花费体力,相当于希望 Impossible
。
此题同样可以压缩到一维数组实现。
参考代码
#include <cstdio> #include <algorithm> using std::max; const int C = 1e4 + 5; int dp[C]; int main() { int v, n, c; scanf("%d%d%d", &v, &n, &c); int ans = -1; for (int i = 1; i <= n; i++) { int val, w; scanf("%d%d", &val, &w); for (int j = c; j >= w; j--) { // 枚举体力j dp[j] = max(dp[j], dp[j - w] + val); if (dp[j] >= v) ans = max(ans, c - j); } } if (ans == -1) printf("Impossible\n"); else printf("%d\n", ans); return 0; }
例题:P1504 积木城堡
对于当前正在考虑的城堡,每块积木只能选去还是一次或不选,所以此题为 01 背包问题。每块积木是否选取会影响城堡的高度,用
用 len
表示第 if (dp[i][j - len]) dp[i][j] = true;
,即如果使用这块积木之前的高度(当前高度减这块积木高度)可以达到,那么当前高度也可以达到。这样使用 01 背包问题的求解思路即可求出当前城堡能达到的所有高度。
现在题目要求的是哪个最大高度是所有城堡都能达到的。可以统计某个高度在这
参考代码
#include <cstdio> #include <algorithm> using std::max; const int N = 105; bool dp[N * N]; int cnt[N * N]; int main() { int n; scanf("%d", &n); int maxsum = 0; for (int i = 1; i <= n; i++) { // n座城堡 int sum = 0; dp[0] = true; // 一开始只有高度为0可以达到 while (true) { // 循环读入积木信息 int len; scanf("%d", &len); if (len == -1) break; sum += len; for (int j = sum; j >= len; j--) { dp[j] |= dp[j - len]; // 如果高度j-len可以到达,那么高度j也能到达 } } for (int j = sum; j >= 0; j--) if (dp[j]) { cnt[j]++; // 可以达到该高度的城堡数量+1 dp[j] = false; // 清空dp数组以备下一个城堡计算时使用 } maxsum = max(maxsum, sum); } int ans = 0; for (int i = maxsum; i >= 0; i--) { // 找到n个城堡都能达成的最大高度 if (cnt[i] == n) { ans = i; break; } } printf("%d\n", ans); return 0; }
习题:CF1954D Colored Balls
解题思路
考虑分组数量的消耗,每一组是最多取
所以按球的数量排序,假设当前枚举的第
参考代码
#include <cstdio> #include <algorithm> using std::sort; typedef long long LL; const int N = 5005; const int MOD = 998244353; int a[N], dp[N][N]; int main() { int n; scanf("%d", &n); for (int i = 1; i <= n; i++) scanf("%d", &a[i]); sort(a + 1, a + n + 1); dp[0][0] = 1; int ans = 0; for (int i = 1; i <= n; i++) { for (int j = 0; j <= 5000; j++) { if (dp[i - 1][j] == 0) continue; if (j <= a[i]) { int add = 1ll * dp[i - 1][j] * a[i] % MOD; ans = (ans + add) % MOD; } else { int add = 1ll * dp[i - 1][j] * ((a[i] + j + 1) / 2) % MOD; ans = (ans + add) % MOD; } } for (int j = 0; j <= 5000; j++) { dp[i][j] = dp[i - 1][j]; if (j >= a[i]) dp[i][j] = (dp[i][j] + dp[i - 1][j - a[i]]) % MOD; } } printf("%d\n", ans); return 0; }
例题:P4377 [USACO18OPEN] Talent Show G
分析:最优比值问题,用 01 分数规划的思想二分答案。
问题变成了选出总重量至少为
可以用背包求出总重量为
最后看
参考代码
#include <cstdio> #include <algorithm> using std::max; using ll = long long; const int N = 255; const int W = 1005; const ll INF = 1e18; int n, wlim, w[N], t[N]; ll dp[W]; bool check(int x) { for (int i = 1; i <= wlim; i++) dp[i] = -INF; for (int i = 1; i <= n; i++) { ll value = t[i] - 1ll * w[i] * x; for (int j = wlim; j >= max(0, wlim - w[i] + 1); j--) if (dp[j] != -INF) dp[wlim] = max(dp[wlim], dp[j] + value); for (int j = wlim; j >= w[i]; j--) if (dp[j - w[i]] != -INF) dp[j] = max(dp[j], dp[j - w[i]] + value); } return dp[wlim] >= 0; } int main() { scanf("%d%d", &n, &wlim); int l = 0, r = 0, ans = 0; for (int i = 1; i <= n; i++) { scanf("%d%d", &w[i], &t[i]); t[i] *= 1000; r = max(r, t[i] / w[i]); } while (l <= r) { int mid = (l + r) / 2; if (check(mid)) { l = mid + 1; ans = mid; } else { r = mid - 1; } } printf("%d\n", ans); return 0; }
多重背包问题
在 01 背包问题中,每种物品只能选
例题:P1776 宝物筛选
一个简单的思路就是转化成熟悉的问题,想办法把多重背包问题转化成 01 背包问题。考虑到既然每种物品有
参考代码
#include <cstdio> #include <algorithm> using std::max; const int N = 105; const int W = 4e5 + 5; int v[N], w[N], m[N], dp[W]; int main() { int n, maxw; scanf("%d%d", &n, &maxw); for (int i = 1; i <= n; i++) scanf("%d%d%d", &v[i], &w[i], &m[i]); for (int i = 1; i <= n; i++) { for (int cnt = 1; cnt <= m[i]; cnt++) { // 拆分成m[i]种只能用一次的物品 for (int j = maxw; j >= w[i]; j--) { dp[j] = max(dp[j], dp[j - w[i]] + v[i]); } } } printf("%d\n", dp[maxw]); return 0; }
分析一下时间复杂度:总的虚拟的物品数量应该是
再考虑另外一种思路:多重决策。多重背包问题和 01 背包问题相比,主要的区别就在于:对于 01 背包问题中的前
对于 01 背包问题,当计算
当前,前提条件是背包容量
参考代码
#include <cstdio> #include <algorithm> using std::max; const int N = 105; const int W = 4e5 + 5; int v[N], w[N], m[N], dp[W]; int main() { int n, maxw; scanf("%d%d", &n, &maxw); for (int i = 1; i <= n; i++) scanf("%d%d%d", &v[i], &w[i], &m[i]); for (int i = 1; i <= n; i++) { for (int j = maxw; j >= w[i]; j--) { for (int cnt = 1; cnt <= m[i] && cnt * w[i] <= j; cnt++) { dp[j] = max(dp[j], dp[j - cnt * w[i]] + cnt * v[i]); } } } printf("%d\n", dp[maxw]); return 0; }
注意两种思路的不同之处。多重决策算法的第
二进制拆分优化。不妨设当前物品的重量为
- 重量是
,价值是 的物品(称为 倍物品) 件; - 重量是
,价值是 的物品(称为 倍物品) 件; - 重量是
,价值是 的物品(称为 倍物品) 件; - 重量是
,价值是 的物品(称为 倍物品) 件。
上面的
为什么这么做是正确的呢?考虑到在最优决策情况下,当前物品所有可能取到的次数是
参考代码
#include <cstdio> #include <algorithm> using std::max; const int N = 105; const int W = 4e5 + 5; int v[N], w[N], m[N], dp[W]; int main() { int n, maxw; scanf("%d%d", &n, &maxw); for (int i = 1; i <= n; i++) scanf("%d%d%d", &v[i], &w[i], &m[i]); for (int i = 1; i <= n; i++) { for (int cnt = 1; cnt <= m[i]; cnt *= 2) { // 当前考虑cnt倍物品,每次循环翻倍,相当于依次枚举1倍物品,2倍物品,4倍物品,…… for (int j = maxw; j >= cnt * w[i]; j--) { // 此时物品的重量是cnt*w[i],价值cnt*v[i] dp[j] = max(dp[j], dp[j - cnt * w[i]] + cnt * v[i]); } m[i] -= cnt; // 从总数m[i]里减掉cnt } for (int j = maxw; j >= m[i] * w[i]; j--) { // 最后剩下一个m[i]倍物品 dp[j] = max(dp[j], dp[j - m[i] * w[i]] + m[i] * v[i]); } } printf("%d\n", dp[maxw]); return 0; }
完全背包问题
01 背包问题是每种物品最多取一次,多重背包问题是每种物品有多件,但是限制了最多使用次数。如果进一步扩展,每种物品的数量都是无限多,这类问题就叫做完全背包问题。
如果还是考虑转化成 01 背包问题,由于背包容量
如果按照多重决策的做法,那么对于每个物品来说,决策就不止取或者不取,而是可以选择:不取,取
也就是在计算
注意,在求
可以看出,当计算
因此,在计算
再回顾一下 01 背包问题的状态转移方程:
可以看到,几乎一样,只不过 01 背包问题是从上一行的位置
要求的是
注意,位置 C 是上一行的第
这样,完全背包问题的代码和 01 背包问题的代码非常接近。考虑能否压缩到一维空间?01 背包问题如果要用一维数组,需要把背包容量的循环按从大到小的顺序进行。因为每次计算均依赖左边的值,必须保证左边的值还没被覆盖成当前物品的值。而完全背包问题的状态转移方程就是要使用当前行左边的值,被覆盖成之后的值正好接下来就要使用,所以完全背包问题压缩空间后的代码,只需要把 01 背包问题代码中背包容量的循环改成正序。
例题:P1616 疯狂的采药
参考代码
#include <cstdio> #include <algorithm> using ll = long long; using std::max; const int M = 1e4 + 5; const int T = 1e7 + 5; int a[M], b[M]; ll dp[T]; int main() { int t, m; scanf("%d%d", &t, &m); for (int i = 1; i <= m; i++) scanf("%d%d", &a[i], &b[i]); for (int i = 1; i <= m; i++) { for (int j = a[i]; j <= t; j++) { dp[j] = max(dp[j], dp[j - a[i]] + b[i]); } } printf("%lld\n", dp[t]); return 0; }
例题:P1679 神奇的四次方数
这个问题可以转化成背包问题。因为要把整数
对于每个四次方数,可以不选,也可以使用无限次,显然这是一个完全背包问题。由于希望使用的数字数量尽可能少,所以
参考代码
#include <cstdio> #include <algorithm> using std::min; const int M = 1e5 + 5; int dp[M]; int main() { int m; scanf("%d", &m); for (int i = 1; i <= m; i++) { dp[i] = i; } for (int i = 1; i * i * i * i <= m; i++) { int num = i * i * i * i; for (int j = num; j <= m; j++) { dp[j] = min(dp[j], dp[j - num] + 1); } } printf("%d\n", dp[m]); return 0; }
分组背包问题
前面介绍的 01 背包问题、多重背包问题和完全背包问题,物品之间都没有关系。一种物品要不要,要几个,都不会影响其他物品的选取。如果物品之间相互影响,比如所有的物品分为
分组背包问题比较好解决,假设所有的物品分为
式中,
伪代码如下:
当前枚举第k组: 倒序枚举背包容量,目前容量为j: 对于每个属于第k组的物品i: dp[j] = max(dp[j], dp[j - w[i]] + v[i])
例题:P1064 [NOIP2006 提高组] 金明的预算方案
首先明确物品的价值是什么,按照本题的定义,每个物品的价值是它的价格和重要度的乘积。所以在输入物品信息时,可以提前计算好这个价值,存在数组里面。而每个物品的价格,其实就相当于 01 背包问题里每个物品的重量,总的花费相当于背包容量。
另外,本题是有依赖的情况,如果要购买附件,就必须购买主件。这个依赖关系可以转化成分组背包问题。对于每个主件,最多有
- 第
个虚拟物品表示只要主件的情况,它的价值对应主件的价值,它的重量对应主件的价格。 - 第
个虚拟物品表示要主件和 号附件的情况,它的价值对应主件和 号附件的价值之和,它的重量对应主件和 号附件的价格之和。 - 第
个虚拟物品表示要主件和 号附件的情况,它的价值对应主件和 号附件的价值之和,它的重量对应主件和 号附件的价格之和。 - 第
个虚拟物品表示要主件和 个附件的情况,它的价值对应主件和 个附件的价值之和,它的重量对应主件和 个附件的价格之和。
上述
参考代码
#include <cstdio> #include <vector> #include <algorithm> using std::max; using std::vector; const int M = 65; const int N = 32005; int v[M], p[M], q[M], dp[N]; vector<int> accessories[M]; void update(int cap, int weight, int value) { if (cap >= weight) dp[cap] = max(dp[cap], dp[cap - weight] + value); } int main() { int n, m; scanf("%d%d", &n, &m); for (int i = 1; i <= m; i++) { scanf("%d%d%d", &v[i], &p[i], &q[i]); // v[i]为价格 p[i] *= v[i]; // 价格和重要度的乘积 if (q[i] != 0) accessories[q[i]].push_back(i); // 如果是附件,在其主件处记录该附件的编号 } for (int i = 1; i <= m; i++) { if (q[i] == 0) { // 每个主件代表一组物品 for (int j = n; j >= 0; j--) { // 枚举背包容量 update(j, v[i], p[i]); // 只使用主件 if (accessories[i].size() > 0) { // 如果有附件 int acs = accessories[i][0]; update(j, v[i] + v[acs], p[i] + p[acs]); // 主件+附件1 } if (accessories[i].size() > 1) { // 如果不止1个附件 int acs1 = accessories[i][0], acs2 = accessories[i][1]; // 主件+附件2 update(j, v[i] + v[acs2], p[i] + p[acs2]); // 主件+2个附件 update(j, v[i] + v[acs1] + v[acs2], p[i] + p[acs1] + p[acs2]); } } } } printf("%d\n", dp[n]); return 0; }
二维费用背包问题
之前背包问题的费用都是一维的,即每个物品有自己的重量。假设每个物品有二维费用,比如每个物品有自己的重量和体积,背包的限制也是二维的,总重量不能超过限制,总体积也不能超过限制,问如何选取物品能使得总价值最大,这就是二维费用背包问题。
既然费用多了一维,那么状态也可以增加一维。设
类似地,可以优化空间复杂度,降一维空间到二维数组。当每件物品只能使用一次时,
例题:P1507 NASA的食物计划
本题可以转化为背包问题:每种食物看作一种物品,食物的重量和体积可看作二维费用,食物的卡路里可看作价值,允许携带的总重量和总体积的限制是背包的两种容量限制。
参考代码
#include <cstdio> #include <algorithm> using std::max; const int H = 405; int dp[H][H]; int main() { int maxh, maxt, n; scanf("%d%d%d", &maxh, &maxt, &n); for (int i = 1; i <= n; i++) { int h, t, cal; scanf("%d%d%d", &h, &t, &cal); for (int j = maxh; j >= h; j--) { for (int k = maxt; k >= t; k--) { dp[j][k] = max(dp[j][k], dp[j - h][k - t] + cal); } } } printf("%d\n", dp[maxh][maxt]); return 0; }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 【杂谈】分布式事务——高大上的无用知识?