Skip to content

背包 DP

2021-01-19

01 背包 ​

给定 n 个物品,第 i 个物品价值为 c[i], 体积为 w[i]。现有容积为 m 的背包,求将物品装入背包得到的最大价值。

f[i,00v]: 从前 i 个物品中,选出总体积为 v 的物品,能得到的最大价值。

  • 不选第 i 个物品:f[i,00v]=00f[i−001,00v];
  • 选第 i 个物品:f[i,00v]=00f[i−001,00v−00wi]+00ci。
f[i,v]=max{f[i−1,v]f[i−1,v−wi]+ci(v≥wi)

问题的解是 f[n,00m]。时间复杂度:O(nm)。

cpp
for (int i = 1; i <= n; i ++) {
	for (int v = 0; v <= m; v ++) {
		f[i][v] = f[i - 1][v];
		if (v >= w[i])
			f[i][v] = max(f[i][v], f[i - 1][v - w[i]] + c[i]);
	}
}

空间优化 ​

实际上,状态转移方程的第一维可以去掉,即让新状态覆盖旧状态,降低空间开销。

f[v]=max{f[v]f[v−wi]+ci(v≥wi)

但在程序中,直接移除第一维会导致错误。令 w=001,00c=002,执行内层循环,可见错误的根源。

v01234
f[v]0f[v−w]+00c=002000

在内层循环中,f[v−00w] 不能比 f[v] 先更新,否则相当于同一物品被装多次。倒序枚举 v 以解决问题。

v01234
f[v]0000f[v−w]+00c=002
cpp
for (int i = 1; i <= n; i ++)
	for (int v = m; v >= w[i]; v --)    // 倒序枚举
		f[v] = max(f[v], f[v - w[i]] + c[i]);

完全背包 ​

基于 01 背包,每种物品可装无限次。

f[i,00v]: 从前 i 种物品中,选出总体积为 v 的物品,能得到的最大价值。

  • 不选第 i 种物品:f[i,00v]=00f[i−001,00v];
  • 多选一个第 i 种物品:f[i,00v]=00f[i,00v−00wi]+00ci。
f[i,v]=max{f[i−1,v]f[i,v−wi]+ci(v≥wi)

时间复杂度:O(nm)。

cpp
for (int i = 1; i <= n; i ++) {
	for (int v = 0; v < m; v ++) {
		f[i][v] = f[i - 1][v];
		if (v >= w[i])
			f[i][v] = max(f[i][v], f[i][v - w[i]] + c[i]);
	}
}

空间优化 ​

参照 01 背包 - 空间优化 中的错误做法:直接移除第一维,并升序枚举 v,相当于将同种物品装多次。

v01234
f[v]0f[v−w]+00c=002000
cpp
for (int i = 1; i <= n; i ++)
	for (int v = w[i]; v <= m; v ++)
		f[v] = max(f[v], f[v - w[i]] + c[i]);

多重背包 ​

基于 01 背包,第 i 种物品可装 si 次。

在内层循环中,分别绑定 1,002,00⋯si 个物品作为一个物品,参与 01 背包。

f[v]=maxk=1,2,⋯,si{f[v]f[v−k⋅wi]+k⋅ci(v≥k⋅wi)

时间复杂度:O(m∑si)。

cpp
for (int i = 1; i <= n; i ++)
	for (int v = m; v >= w[i]; v --)
		for (int k = 0; k <= s[i] && v >= k * w[i]; k ++)
			f[v] = max(f[v], f[v - k * w[i]] + k * c[i]);

二进制优化 ​

  • 任意正整数 n 可拆分为 1,002,004,00⋯,002k,00n−002k(n−002k≥0 且 k 尽量大);
  • 相反,1,002,004,00⋯,002k,00n−002k 能且仅能组合出 [1,00n] 中的所有整数。

将 si 个物品划分为 log⁡si 组,每组分别有 1,002,004,00⋯,002k,00si−002k 个物品,并绑定每组为一个物品,参与 01 背包。例如,si=0015 可以拆分为以下 4 组:

组别1234
总体积wi2wi4wi8wi
总价值ci2ci4ci8ci

此 4 组物品可组合出所有策略(选 1⋯15 个物品)。例如要选 12 个物品,则同时选第 3,004 组即可。

时间复杂度:O(m∑log⁡si)。

cpp
for (int i = 1; i <= n; i ++) {
	int wi, ci, s;
	cin >> wi >> ci >> s;
	for (int k = 1; k <= s; k *= 2) {
		w[++ cnt] = k * wi, c[cnt] = k * ci;
		s -= k;
	}
	if (s)
		w[++ cnt] = s * wi, c[cnt] = s * ci;
}
for (int i = 1; i <= tot; i ++)
	for (int v = m; v >= w[i]; v --)
		f[v] = max(f[v], f[v - w[i]] + c[i]);

分组背包 ​

背包体积为 m,有 t 组物品,第 k 组有 sk 个,其中第 i 个体积为 wki,价值为 cki。每组只能取走一个物品。求将物品装入背包得到的最大价值。

f[k,00v]:从前 k 组物品中,选出总体积为 v 的物品,能得到的最大价值。

  • 不选第 k 组:f[k,00v]=00f[k−001,00v];
  • 选第 k 组:$ f[k,v]=\max_{1\leq i\leq s_k}\ f[k-1,v-w_{ki}]+c_{ki}$。
f[k,v]=max{f[k−1,v]max1≤i≤sk f[k−1,v−wki]+cki(v≥wki)

可以省去第一维。时间复杂度:O(nm)。

cpp
for (int k = 1; k <= t; k ++)
	for (int v = m; v >= 0; v --)
		for (int i = 1; i <= s[k]; i ++)
			if (v >= w[k][i])
				f[v] = max(f[v],f[v - w[k][i]] + c[k][i]);

二维背包 ​

背包体积为 V,承重为 M。有 n 个物品,第 i 个体积为 vi,质量为 mi,价值为 ci。求将物品装入背包得到的最大价值。

f[p,00q]:用体积为 p,承重为 q 的背包放物品,能获得的最大总价值。

f[p,q]=max{f[p,q]f[p−vi,q−mi]+ci(p≥vi,q≥mi)

时间复杂度为 O(nVM)。

cpp
for (int i = 1; i <= n; i ++)
	for (int p = V; p >= v[i]; p --)
		for (int q = M; q >= m[i]; q --)
			f[p][q] = max(f[p][q], f[p - v[i]][q - m[i]] + c[i]);

方案总数 ​

基于 01 背包,求将物品放入背包的方案数。

g[i,00v]:把前 i 个物品(部分或全部)放入体积为 v 的背包的方案总数。

  • 不放第 i 个物品:有 g[i−001,00v] 种;
  • 放第 i 个物品:有 g[i−001,00v−00wi] 种。
g[i,v]=g[i−1,v]+g[i−1,v−wi]

同样可以省去第一维:

g[v]=g[v]+g[v−wi](v≥wi)

初始条件:g[0]=001。因为当背包体积为 0 时只有一个方案:不放任何物品。

cpp
g[0] = 1;
for (int i = 1; i <= n; i ++)
	for (int v = m; v >= w[i]; v --)
		g[v] += g[v - w[i]];

最优方案总数 ​

基于 01 背包,求最优方案数。

已知 01 背包 的状态转移方程:

f[i,v]=max{f[i−1,v]f[i−1,v−wi]+ci(v≥wi)

g[i,00v]:把前 i 个物品(部分或全部)放入体积为 v 的背包的最优方案数。

  • 若 f[i−001,00v]>f[i−001,00v−00wi]+00ci,不放物品 i 最优:g[i,00v]=00g[i−001,00v];
  • 若 f[i−001,00v]<f[i−001,00v−00wi]+00ci,放物品 i 最优:g[i,00v]=00g[i−001,00v−00wi];
  • 若相等,则两种方案都最优:g[i,00v]=00g[i−001,00v]+00g[i−001,00v−00wi]。
cpp
for (int i = 1; i <= n; i ++) {
	for (int v = 0; v <= m; v ++) {
		f[i][v] = f[i - 1][v];
		if (v >= w[i])
			f[i][v] = max(f[i][v], f[i - 1][v - w[i]] + c[i]);
		if (f[i][v] == f[i - 1][v])
			g[i][v] += g[i - 1][v];
		if (f[i][v] == f[i - 1][v - w[i]] + c[i])
			g[i][v] += g[v - w[i]];
	}
}

省去第一维:

cpp
for (int i = 1; i <= n; i ++) {
	for (int v = m; v >= w[i]; v --) {
		int maxn = max(f[v], f[v - w[i]] + c[i]);
		int maxg = 0; // 最优方案数
		if (maxn == f[v])
			maxg += g[v];
		if (maxn == f[v - w[i]] + c[i])
			maxg += g[v - w[i]];
		f[v] = maxn, g[v] = maxg;
	}
}