背包 DP
在学习本章前请确认你已经学习了 动态规划部分简介
在具体讲何为「背包 dp」前,先来看如下的例题:
「USACO07 DEC」Charm Bracelet
题意概要:有
在上述例题中,由于每个物体只有两种可能的状态(取与不取),对应二进制中的
0-1 背包¶
例题中已知条件有第
设 DP 状态
考虑转移。假设当前已经处理好了前
由此可以得出状态转移方程:
这里如果直接采用二维数组对状态进行记录,会出现 MLE。可以考虑改用滚动数组的形式来优化。
由于对
务必牢记并理解这个转移方程,因为大部分背包问题的转移方程都是在此基础上推导出来的。
还有一点需要注意的是,很容易写出这样的错误核心代码:
1 2 3 4 5 6 | // C++ Version
for (int i = 1; i <= n; i++)
for (int l = 0; l <= W - w[i]; l++)
f[l + w[i]] = max(f[l] + v[i], f[l + w[i]]);
// 由 f[i][l + w[i]] = max(max(f[i - 1][l + w[i]],f[i - 1][l] + w[i]),f[i][l +
// w[i]]); 简化而来
|
1 2 3 4 5 6 7 8 | # Python Version
for i in range(1, n + 1):
l = 0
while l <= W - w[i]:
f[l + w[i]] = max(f[l] + v[i], f[l + w[i]])
l += 1
# 由 f[i][l + w[i]] = max(max(f[i - 1][l + w[i]],f[i - 1][l] + w[i]),f[i][l +
# w[i]]) 简化而来
|
这段代码哪里错了呢?枚举顺序错了。
仔细观察代码可以发现:对于当前处理的物品
为了避免这种情况发生,我们可以改变枚举的顺序,从
因此实际核心代码为
1 2 3 | // C++ Version
for (int i = 1; i <= n; i++)
for (int l = W; l >= w[i]; l--) f[l] = max(f[l], f[l - w[i]] + v[i]);
|
1 2 3 4 5 6 | # Python Version
for i in range(1, n + 1):
l = W
while l >= w[i]:
f[l] = max(f[l], f[l - w[i]] + v[i])
l -= 1
|
例题代码
1 2 3 4 5 6 7 8 9 10 11 12 13 | #include <iostream>
using namespace std;
const int maxn = 13010;
int n, W, w[maxn], v[maxn], f[maxn];
int main() {
cin >> n >> W;
for (int i = 1; i <= n; i++) cin >> w[i] >> v[i]; //读入数据
for (int i = 1; i <= n; i++)
for (int l = W; l >= w[i]; l--)
if (f[l - w[i]] + v[i] > f[l]) f[l] = f[l - w[i]] + v[i]; //状态方程
cout << f[W];
return 0;
}
|
完全背包¶
完全背包模型与 0-1 背包类似,与 0-1 背包的区别仅在于一个物品可以选取无限次,而非仅能选取一次。
我们可以借鉴 0-1 背包的思路,进行状态定义:设
需要注意的是,虽然定义与 0-1 背包类似,但是其状态转移方程与 0-1 背包并不相同。
可以考虑一个朴素的做法:对于第
状态转移方程如下:
考虑做一个简单的优化。可以发现,对于
理由是当我们这样转移时,
与 0-1 背包相同,我们可以将第一维去掉来优化空间复杂度。如果理解了 0-1 背包的优化方式,就不难明白压缩后的循环是正向的(也就是上文中提到的错误优化)。
「Luogu P1616」疯狂的采药
题意概要:有
例题代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | #include <iostream>
using namespace std;
const int maxn = 1e4 + 5;
const int maxW = 1e7 + 5;
int n, W, w[maxn], v[maxn];
long long f[maxW];
int main() {
cin >> W >> n;
for (int i = 1; i <= n; i++) cin >> w[i] >> v[i];
for (int i = 1; i <= n; i++)
for (int l = w[i]; l <= W; l++)
if (f[l - w[i]] + v[i] > f[l]) f[l] = f[l - w[i]] + v[i]; //核心状态方程
cout << f[W];
return 0;
}
|
多重背包¶
多重背包也是 0-1 背包的一个变式。与 0-1 背包的区别在于每种物品有
一个很朴素的想法就是:把「每种物品选
时间复杂度
二进制分组优化¶
考虑优化。我们仍考虑把多重背包转化成 0-1 背包模型来求解。
显然,复杂度中的
在朴素的做法中,
我们可以通过「二进制分组」的方式使拆分方式更加优美。
具体地说就是令
举几个例子:
6=1+2+3 8=1+2+4+1 18=1+2+4+8+3 31=1+2+4+8+16
显然,通过上述拆分方式,可以表示任意
时间复杂度
二进制分组代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | // C++ Version
index = 0;
for (int i = 1; i <= m; i++) {
int c = 1, p, h, k;
cin >> p >> h >> k;
while (k - c > 0) {
k -= c;
list[++index].w = c * p;
list[index].v = c * h;
c *= 2;
}
list[++index].w = p * k;
list[index].v = h * k;
}
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | # Python Version
index = 0
for i in range(1, m + 1):
c = 1
p, h, k = map(lambda x:int(x), input().split())
while k - c > 0:
k -= c
list[index].w = c * p
index += 1
list[index].v = c * h
c *= 2
list[index].w = p * k
index += 1
list[index].v = h * k
|
单调队列优化¶
见 单调队列/单调栈优化。
习题:「Luogu P1776」宝物筛选_NOI 导刊 2010 提高(02)
混合背包¶
混合背包就是将前面三种的背包问题混合起来,有的只能取一次,有的能取无限次,有的只能取
这种题目看起来很吓人,可是只要领悟了前面几种背包的中心思想,并将其合并在一起就可以了。下面给出伪代码:
1 2 3 4 5 6 7 8 | for (循环物品种类) {
if (是 0 - 1 背包)
套用 0 - 1 背包代码;
else if (是完全背包)
套用完全背包代码;
else if (是多重背包)
套用多重背包代码;
}
|
「Luogu P1833」樱花
题意概要:有
二维费用背包¶
先来一道例题:「Luogu P1855」榨取 kkksc03。
这道题是很明显的 0-1 背包问题,可是不同的是选一个物品会消耗两种价值(经费、时间)。这种问题其实很简单:方程基本不用变,只需再开一维数组,同时转移两个价值就行了!(完全、多重背包同理)
这时候就要注意,再开一维存放物品编号就不合适了,因为容易 MLE。
例题核心代码:
1 2 3 4 5 6 | // C++ Version
for (int k = 1; k <= n; k++) {
for (int i = m; i >= mi; i--) // 对经费进行一层枚举
for (int j = t; j >= ti; j--) // 对时间进行一层枚举
dp[i][j] = max(dp[i][j], dp[i - mi][j - ti] + 1);
}
|
1 2 3 4 5 6 7 8 9 | # Python Version
for k in range(1, n + 1):
i = m
while i >= mi: # 对经费进行一层枚举
j = t
while j >= ti: # 对时间进行一层枚举
dp[i][j] = max(dp[i][j], dp[i - mi][j - ti] + 1)
j -= 1
i -= 1
|
分组背包¶
再看一道例题:「Luogu P1757」通天之分组背包。
所谓分组背包,就是将物品分组,每组的物品相互冲突,最多只能选一个物品放进去。
这种题怎么想呢?其实是从「在所有物品中选择一件」变成了「从当前组中选择一件」,于是就对每一组进行一次 0-1 背包就可以了。
再说一说如何进行存储。我们可以将
例题核心代码:
1 2 3 4 5 6 7 | // C++ Version
for (int k = 1; k <= ts; k++) // 循环每一组
for (int i = m; i >= 0; i--) // 循环背包容量
for (int j = 1; j <= cnt[k]; j++) // 循环该组的每一个物品
if (i >= w[t[k][j]])
dp[i] = max(dp[i],
dp[i - w[t[k][j]]] + c[t[k][j]]); // 像0-1背包一样状态转移
|
1 2 3 4 5 6 7 | # Python Version
for k in range(1, ts + 1): # 循环每一组
for i in range(m, -1, -1): # 循环背包容量
for j in range(1, cnt[k] + 1): # 循环该组的每一个物品
if i >= w[t[k][j]]:
dp[i] = max(dp[i], \
dp[i - w[t[k][j]]] + c[t[k][j]]) # 像0-1背包一样状态转移
|
这里要注意:一定不能搞错循环顺序,这样才能保证正确性。
有依赖的背包¶
一道例题:「Luogu P1064」金明的预算方案。
这种背包问题其实就是如果选第
对于包含一个主件和若干个附件的集合有以下可能性:仅选择主件,选择主件后再选择一个附件,选择主件后再选择两个附件……需要将以上可能性的容量和价值转换成一件件物品。因为这几种可能性只能选一种,所以可以将这看成分组背包。
如果是多叉树的集合,则要先算子节点的集合,最后算父节点的集合。
泛化物品的背包¶
这种背包,没有固定的费用和价值,它的价值是随着分配给它的费用而定。在背包容量为
杂项¶
小优化¶
根据贪心原理,当费用相同时,只需保留价值最高的;当价值一定时,只需保留费用最低的;当有两件物品
背包问题变种¶
输出方案¶
输出方案其实就是记录下来背包中的某一个状态是怎么推出来的。我们可以用
1 2 3 4 5 6 7 8 9 10 | int v = V; // 记录当前的存储空间
// 因为最后一件物品存储的是最终状态,所以从最后一件物品进行循环
for (从最后一件循环至第一件) {
if (g[i][v]) {
选了第 i 项物品;
v -= 第 i 项物品的价值;
} else
未选第 i 项物品;
}
|
求方案数¶
对于给定的一个背包容量、物品费用、其他关系等的问题,求装到一定容量的方案总数。
这种问题就是把求最大值换成求和即可。
例如 0-1 背包问题的转移方程就变成了:
初始条件:
因为当容量为
求最优方案总数¶
要求最优方案总数,我们要对 0-1 背包里的
这样修改之后,每一种 DP 状态都可以用一个
转移方程:
如果
如果
如果
初始条件:
1 2 3 | memset(f, 0x3f3f, sizeof(f)); // 避免没有装满而进行了转移
f[0] = 0;
g[0] = 1; // 什么都不装是一种方案
|
因为背包体积最大值有可能装不满,所以最优解不一定是
最后我们通过找到最优解的价值,把
核心代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | for (int i = 0; i < N; i++) {
for (int j = V; j >= v[i]; j--) {
int tmp = max(dp[j], dp[j - v[i]] + w[i]);
int c = 0;
if (tmp == dp[j]) c += cnt[j]; // 如果从dp[j]转移
if (tmp == dp[j - v[i]] + w[i]) c += cnt[j - v[i]]; // 如果从dp[j-v[i]]转移
dp[j] = tmp;
cnt[j] = c;
}
}
int max = 0; // 寻找最优解
for (int i = 0; i <= V; i++) {
max = max(max, dp[i]);
}
int res = 0;
for (int i = 0; i <= V; i++) {
if (dp[i] == max) {
res += cnt[i]; // 求和最优解方案数
}
}
|
背包的第 k 优解¶
普通的 0-1 背包是要求最优解,在普通的背包 DP 方法上稍作改动,增加一维用于记录当前状态下的前 k 优解,即可得到求 0-1 背包第
例题 hdu 2639 Bone Collector II
求 0-1 背包的严格第
核心代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | memset(dp, 0, sizeof(dp));
int i, j, p, x, y, z;
scanf("%d%d%d", &n, &m, &K);
for (i = 0; i < n; i++) scanf("%d", &w[i]);
for (i = 0; i < n; i++) scanf("%d", &c[i]);
for (i = 0; i < n; i++) {
for (j = m; j >= c[i]; j--) {
for (p = 1; p <= K; p++) {
a[p] = dp[j - c[i]][p] + w[i];
b[p] = dp[j][p];
}
a[p] = b[p] = -1;
x = y = z = 1;
while (z <= K && (a[x] != -1 || b[y] != -1)) {
if (a[x] > b[y])
dp[j][z] = a[x++];
else
dp[j][z] = b[y++];
if (dp[j][z] != dp[j][z - 1]) z++;
}
}
}
printf("%d\n", dp[m][K]);
|
参考资料与注释¶
build本页面最近更新:,更新历史
edit发现错误?想一起完善? 在 GitHub 上编辑此页!
people本页面贡献者:hydingsy, Link-cute, Ir1d, greyqz, LuoshuiTianyi, Odeinjul
copyright本页面的全部内容在 CC BY-SA 4.0 和 SATA 协议之条款下提供,附加条款亦可能应用