01 背包
问题形式
种物品,第 种重量为 ,价值为 ,每种物品只有一个。有一个容量为 的背包,让你选择一些物品放进背包,在物品重量和 的情况下使物品价值和最大。
状态与转移
设 为前 个物品中选重量和为 的物品的最大价值和。
优化
滚动数组
我们可以将 维舍弃,并对 维使用倒序循环(为了不让 从 转移)。
完全背包
问题形式
种物品,第 种重量为 ,价值为 ,每种物品有无数个。有一个容量为 的背包,让你选择一些物品放进背包,在物品重量和 的情况下使物品价值和最大。
状态与转移
设 为前 个物品中选重量和为 的物品的最大价值和。
优化
滚动数组
我们可以将 维舍弃,并对 维使用正序循环(为了让 从 转移)。
多重背包
种物品,第 种重量为 ,价值为 ,有 个。有一个容量为 的背包,让你选择一些物品放进背包,在物品重量和 的情况下使物品价值和最大。
最直接的方法是将每一种物品拆成 个一样的物品然后使用 01 背包,时间复杂度为 。
优化
滚动数组
同 01 背包。
二进制拆分
对于每一种物品,设 为满足 的最大值, 为 ,我们可以将物品拆分为 。
这样可以保证这 个物品可以组成 的所有情况,时间复杂度为 。
单调队列优化
不优化的转移是这样的:
这个式子很难优化,我们发现 总是会从 转移,所以我们可以将 按照 的余数分组,因为不同组之间不会互相影响,所以可以对每一组分别 DP。(代表余数),转移方程变成这样:
这样子就可以使用单调队列优化了,时间复杂度为 。
注意,余数 的循环顺序没有影响(每一组不会互相转移),但是 必须要倒着循环(原理同 01 背包)。
具体代码实现在这里:
#include <algorithm>
#include <cstdio>
using namespace std;
int n, m, v[1001], w[1001], c[1001], f[20001], ans;
int s, t, q[20001];
int main(){
scanf("%d%d", &n, &m);
for (int i = 1;i <= n;i ++) scanf("%d%d%d", &v[i], &w[i], &c[i]), c[i] = min(c[i], m / v[i]);
for (int i = 1;i <= n;i ++){
for (int j = 0;j < v[i];j ++){ // 枚举余数 j
#define calc(x) (f[j + (x) * v[i]] - (x) * w[i])
s = 1, t = 0;
int p = (m - j) / v[i]; // 最大能选取的 k
for (int k = p - 1;k >= max(p - c[i], 0);k --){
while (s <= t && calc(q[t]) <= calc(k)) t --;
q[++ t] = k;
} // 先将初始决策插入单调队列(k = p 时需要的决策)
for (int k = p;k >= 0;k --){
while (s <= t && q[s] > k - 1) s ++; // 删除不合法的决策
if (s <= t) f[j + k * v[i]] = max(f[j + k * v[i]], calc(q[s]) + w[i] * k); // 转移
if (k - c[i] - 1 < 0) continue;
while (s <= t && calc(q[t]) <= calc(k - c[i] - 1)) t --; // 维护单调性
q[++ t] = k - c[i] - 1; // 插入新决策
}
}
}
for (int j = 0;j <= m;j ++) ans = max(ans, f[j]);
printf("%d", ans);
}
分组背包
组物品,第 组内有 种物品。第 组内第 种物品重量为 ,价值为 ,每种物品只有一个。有一个容量为 的背包,让你选择一些物品放进背包,在满足物品重量和 且每一组物品最多只放一个的情况下使物品价值和最大。
状态与转移
设 为前 组物品中选重量和为 的物品的最大价值和。
优化
滚动数组
同 01 背包。