Appearance
其实感觉背包只是一种特别的、人为总结的 dp 问题,在 dp 水平足够之后,不需要专门地学习,也能得到其绝大多数的做法。
背包 dp 更多担任的是引导入门 dp 的角色,可以通过一系列的背包 dp 帮助新手了解 dp 思想,熟悉 dp。
以下最大价值类模板默认所有物品体积为正整数,并令 dp 数组初始化为 dp[0] = 0、其余状态初始化为 -INF,并只从可达状态转移;后文的可行性背包和方案数背包则默认讨论恰好达到对应体积。
01 背包
点击展开代码
cpp
for (int i = 1; i <= n; i++)
for (int j = V; j >= v[i]; j--)
dp[j] = max(dp[j], dp[j - v[i]] + w[i]);时间复杂度:
空间复杂度:
完全背包
点击展开代码
cpp
for (int i = 1; i <= n; i++)
for (int j = v[i]; j <= V; j++)
dp[j] = max(dp[j], dp[j - v[i]] + w[i]);时间复杂度:
空间复杂度:
多重背包
点击展开代码
cpp
for (int i = 1; i <= n; i++)
for (int j = V; j >= v[i]; j--)
for (int k = 1; k <= min(s[i], j / v[i]); k++)
dp[j] = max(dp[j], dp[j - k * v[i]] + k * w[i]);时间复杂度:
空间复杂度:
二进制优化
点击展开代码
cpp
for (int i = 1; i <= n; i++) {
int cnt = min(s[i], V / v[i]);
int num = 1;
while (cnt) {
int take = min(num, cnt);
cnt -= take;
for (int k = V; k >= take * v[i]; k--)
dp[k] = max(dp[k], dp[k - take * v[i]] + take * w[i]);
if (num <= cnt)
num <<= 1;
}
}时间复杂度:
空间复杂度:
单调队列优化
点击展开代码
cpp
for (int i = 1; i <= n; i++) {
int now = (i & 1);
int cur = (now ^ 1);
int cnt = min(s[i], V / v[i]);
dp[now] = dp[cur];
for (int j = 0; j < v[i]; j++) {
q.clear();
for (int k = j; k <= V; k += v[i]) {
int T = k / v[i];
while (q.size() && q.front().first < k - cnt * v[i])
q.pop_front();
if (q.size())
dp[now][k] = max(dp[now][k], q.front().second + T * w[i]);
while (q.size() && q.back().second < dp[cur][k] - T * w[i])
q.pop_back();
q.push_back({k, dp[cur][k] - T * w[i]});
}
}
}时间复杂度:
空间复杂度:
注:
- 实现上的一些细节,对于每个余数单独跑一遍,只需要开一个单调队列,常数更小。
- 因为常数问题,实现不好的单调队列优化很可能跑不过二进制优化。
分组背包
点击展开代码
cpp
for (int i = 1; i <= n; i++)
for (int j = V; j >= 0; j--)
for (int k = 1; k <= m[i]; k++)
if (j >= v[i][k])
dp[j] = max(dp[j], dp[j - v[i][k]] + w[i][k]);时间复杂度:
空间复杂度:
多维背包
点击展开代码
cpp
for (int i = 1; i <= n; i++)
for (int j = V1; j >= v1[i]; j--)
for (int k = V2; k >= v2[i]; k--)
dp[j][k] = max(dp[j][k], dp[j - v1[i]][k - v2[i]] + w[i]);时间复杂度:
空间复杂度:
混合背包
这个没有固定形式,原则上可以把上述所有背包问题全部杂合在一起,每种物品都可以是上面任意一种类型。
可行性背包
点击展开代码
cpp
dp[0] = 1;
for (int i = 1; i <= n; i++)
for (int j = V; j >= v[i]; j--)
dp[j] |= dp[j - v[i]];时间复杂度:
空间复杂度:
bitset 优化
点击展开代码
cpp
dp[0] = 1;
for (int i = 1; i <= n; i++)
dp |= (dp << v[i]);时间复杂度:
空间复杂度:
限和背包的二进制优化
可以发现,本质上,01 背包和完全背包都是多重背包。
对于 01 背包,可以把体积、价值相同的物品合并,视作多重背包,从而应用二进制优化。
结论:对于一个体积为
的可行性背包,若所有物品的体积之和不超过 ,则使用二进制优化的时间复杂度为 ,使用 bitset还可以进一步优化到。
证明:根号分治,对于价值
对于价值
应用拉格朗日乘子法:
令
代入反解
此时,
根据斯特林近似:
代回,可得
综上:对于价值
背包求方案数
以 01 背包为例:
点击展开代码
cpp
dp[0] = 1;
for (int i = 1; i <= n; i++)
for (int j = V; j >= v[i]; j--)
dp[j] += dp[j - v[i]];时间复杂度:
空间复杂度:
卷积优化
每一个物品可以视作一个形式幂级数,
若物品的体积为
按
时间复杂度:
空间复杂度:
可撤销性
多项式角度
删除一个物品,相当于少乘一个形式幂级数,做一次多项式除法即可。
若是 01 背包,使用长除法,撤销单个物品就是
dp 角度
删除一个物品,把这个物品的贡献减掉即可。
注意枚举顺序的影响:若是 01 背包,
01 背包:
点击展开代码
cpp
for (int j = v[i]; j <= V; j++)
dp[j] -= dp[j - v[i]];完全背包:
点击展开代码
cpp
for (int j = V; j >= v[i]; j--)
dp[j] -= dp[j - v[i]];多重背包的每个二进制拆分组都可以和 01 背包一样处理。
若要在
因此按
点击展开代码
cpp
if (s[i]) {
for (int j = 0; j < v[i]; j++) {
int sum = dp[j];
for (int k = j + v[i]; k <= V; k += v[i]) {
dp[k] -= sum;
if (k / v[i] >= s[i])
sum -= dp[k - s[i] * v[i]];
sum += dp[k];
}
}
}代码执行到当前体积前,sum 始终是递推所需的最近
01 背包、完全背包以及上述直接滑动窗口写法,撤销一次加入操作的时间复杂度均为
前后缀背包
令 pre[i][j] 表示只使用前 suf[i][j] 表示只使用第
作用是,将前缀
预处理时间复杂度:
点击展开代码
cpp
vector<int> ans(V + 1, -INF);
for (int j = 0; j <= V; j++)
for (int k = 0; k <= j; k++)
if (pre[i - 1][j - k] != -INF && suf[i + 1][k] != -INF)
ans[j] = max(ans[j], pre[i - 1][j - k] + suf[i + 1][k]);
dp = ans;拼接时必须始终从固定的前缀表和后缀表读取,不能一边写 dp 一边再把它作为前缀状态读取,否则会重复使用后缀物品。求出完整结果数组的单次拼接是 max-plus 卷积,时间复杂度为
树上背包
下面的模板令 dp[x][j] 表示在 -INF。
点击展开代码
cpp
sz[x] = 1;
fill(dp[x], dp[x] + V + 1, -INF);
dp[x][1] = w[x];
for (auto u : p[x]) {
dfs(u);
for (int i = min(sz[x], V); i >= 1; i--) {
for (int j = min(sz[u], V - i); j >= 1; j--) {
if (dp[x][i] != -INF && dp[u][j] != -INF)
dp[x][i + j] = max(dp[x][i + j], dp[x][i] + dp[u][j]);
}
}
sz[x] += sz[u];
}对于上述“单位体积、按选点数计”的模板,利用子树大小限制枚举上下界后,总时间复杂度可以分析为
空间复杂度:
通过链剖分结合 NTT 的科技,可以做到
有依赖的背包
有依赖的背包是一种特殊的树上背包,注意祖先节点一定要选。
可行性完全背包的最少物品数
对于一个可行性完全背包,求最少拿几个物品可以恰好得到体积
点击展开代码
cpp
fill(dp, dp + V + 1, INF);
dp[0] = 0;
for (int i = 1; i <= n; i++)
for (int j = v[i]; j <= V; j++)
if (dp[j - v[i]] != INF)
dp[j] = min(dp[j], dp[j - v[i]] + 1);时间复杂度为 dp[V] == INF,则体积
倍增 NTT
令
若要恢复单调性,应令
其中
预处理
若用单模 NTT 实现布尔卷积,需要选择支持所需变换长度且满足
