01背包
二维板子
#include <bits/stdc++.h>
using namespace std;
const int N = 1e3 + 10;
int n,V;
int v[N], w[N];
int f[N][N];
int main() {
ios::sync_with_stdio(0);
cin >> n >> V; //物品个数 背包容量总数
//朴素分析
for (int i = 1; i <= n; ++i) {
cin >> v[i] >> w[i];//体积 价值
}
for (int i = 1; i <= n; ++i) {
for (int j = 0; j <= V; ++j) {
f[i][j] = f[i - 1][j];
if (j >= w[i])
f[i][j] = max(f[i][j], f[i - 1][j - v[i]] + w[i]);
}
}
cout << f[n][V] << endl;
return 0;
}
一维板子1
保证 f ≥ 0 f\ge0 f≥0 时, f j f_j fj 表示体积不超过 j j j 时的最大价值
#include <bits/stdc++.h>
using namespace std;
const int N = 1e3 + 10;
int n,V;
int v[N], w[N];
int f[N];
int main() {
ios::sync_with_stdio(0);
cin >> n >> V; //物品个数 背包容量总数
for (int i = 1; i <= n; ++i) {
cin >> v[i] >> w[i];//体积 价值
}
//01背包没办法在时间上优化 只能在空间上进行优化
//发现f[i][j]的值只与i-1层有关 且f[i][j]=min(f[i-1][j],f[i-1][j-v[i]])
//中可以看到f[i][j]更新时取得值 要么是上一层同样是j容量的位置 要么是更小的容量j-v[i]的位置
//所以可以省掉第一位的空间 并且选择 从后往前滚动数组 达到空间上的优化
for (int i = 1; i <= n; ++i) {
for (int j = V; j >= v[i]; --j) {
f[j] = max(f[j], f[j - v[i]] + w[i]);
}
}
cout << f[V] << endl;
return 0;
}
有些01背包问题里,最大价值可能为负数,此时
f
j
f_j
fj 就不可以表示为体积不超过
j
j
j 时的最大价值了
可以通过打表,体会差别
此时的板子应该长这样
一维板子2
#include <bits/stdc++.h>
using namespace std;
const int N = 1e3 + 10;
const int INF = 0x3f3f3f3f;
//注意 0x3f 0x3f3f 0x3f3f3f 0x3f3f3f3f 是四个不同的数
int n, V;
int v[N], w[N];
//差别1:f[j]表示体积为j下的最大价值 (少了'不超过')
int f[N];
int main() {
ios::sync_with_stdio(0);
cin >> n >> V; //物品个数 背包容量总数
for (int i = 1; i <= n; ++i) {
cin >> v[i] >> w[i];//体积 价值
}
//差别2:初始化不同
memset(f, -INF, sizeof(f));
f[0] = 0;
for (int i = 1; i <= n; ++i) {
for (int j = V; j >= v[i]; --j) {
f[j] = max(f[j], f[j - v[i]] + w[i]);
}
}
//差别3:需要遍历f[] 找出其中的最优解
int res = -INF;
for (int i = 1; i <= V; ++i) {
res = max(res, f[i]);
}
cout << res<< endl;
return 0;
}
应用:
完全背包
一维板子
#include <bits/stdc++.h>
using namespace std;
const int N = 1e3 + 10;
int n,V;
int v[N], w[N];
int f[N];
int main() {
ios::sync_with_stdio(0);
cin >> n >> V; //物品个数 背包容量总数
for (int i = 1; i <= n; ++i) {
cin >> v[i] >> w[i];//体积 价值
}
//完全背包问题 只需要在01背包的基础上改为让j从小到大即可
for (int i = 1; i <= n; ++i) {
for (int j = v[i]; j <= V; ++j) {
f[j] = max(f[j], f[j - v[i]] + w[i]);
}
}
cout << f[V] << endl;
return 0;
}
多重背包
每个物品s件,体积v,价值w
//核心代码
for (int i = 1; i <= n; ++i) {
for (int j = V; j >= v; --j) {
f[j] = max(f[j], f[j - v] + w, f[j - 2v] + 2w, ..., f[j - sv] + sw);
}
}
普通板子
// O(n·m·s)
// n=100 m=100 s=100
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1e6 + 10;
int n, m;
int v, w, s;
int f[N];//f[i]表示为体积不超过i的最大价值
int main() {
ios::sync_with_stdio(0);
cin >> n >> m;
for (int i = 1; i <= n; ++i) {
cin >> v >> w >> s; // 体积 价值 个数
for (int j = m; j >= v; --j) { //多重背包是01背包的扩展 所以是从大到小枚举的
for (int k = 1; k <= s && k * v <= j; ++k) { // 0~s 总共s+1种决策
f[j] = max(f[j], f[j - k * v] + k * w);
}
}
}
cout << f[m] << endl;
return 0;
}
二进制优化版
AcWing 5. 多重背包问题 II
用最少的数表示1~x:
⌈
log
2
(
x
)
⌉
\left\lceil\log_2(x)\right\rceil
⌈log2(x)⌉
as:最少需要4个数:1、2、4、3 (不是8) 才能表示1~10所有的数
// O(n·log(s)·m)
// n=1000 m=1000 s=2000
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1e6 + 10;
int n, m, K;
struct Good {
int v, w;
};
int f[N];
int main() {
ios::sync_with_stdio(0);
vector<Good> goods;
cin >> n >> m;
for (int i = 1; i <= n; ++i) {
int v, w, s;
cin >> v >> w >> s;
for (int k = 1; k <= s; k *= 2) { //将s拆开用最少的数表示 部分倍数就可以用01背包统计
s -= k; //每次统计都需要-k
goods.push_back({v * k, w * k});
}
if (s > 0)
goods.push_back({v * s, w * s});
}
for (auto good:goods) {
for (int j = m; j >= good.v; --j) {
f[j] = max(f[j], f[j - good.v] + good.w);
}
}
cout << f[m] << endl;
return 0;
}
单调队列优化版
// O(nm)
// n=10000 m=20000 s=20000
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1e6 + 10;
int n, m, K;
int f[N], g[N];
int q[N];//单调队列 存的是最优解的体积 f[队首]为最优解
int main() {
ios::sync_with_stdio(0);
cin >> n >> m;
for (int i = 1; i <= n; ++i) {
int v, w, s;
cin >> v >> w >> s;
memcpy(g, f, sizeof(f));//更新上一层所有值
for (int j = 0; j < v; ++j) { //枚举所有的余数 0~v-1
// j个单调队列
int h = 0;//单调队列的首尾
int t = -1;
for (int k = j; k <= m; k += v) { //枚举所有余数相同的体积
if (h <= t && (k - q[h]) / v > s) h++;// (当前体积-队首体积)/单份体积=倍数 要小于等于倍数限制s
if (h <= t) f[k] = max(f[k], g[q[h]] + (k - q[h]) / v * w);// f[k]=max(f[k],f[j]+(k-j)/v*w);
while (h <= t && g[q[t]] - (q[t] - j) / v * w <= g[k] - (k - j) / v * w) t--;
q[++t] = k;
}
}
}
cout << f[m] << endl;
/*
* f[j]= max(f[j-v]+w, f[j-2v]+2w, f[j-3v]+3w.....)
* 可以看到f[j]的值只和这些数有关 而这些数都有一个共同的特点:其下标%v的余数相同
* 当统计f[j+v]的时候 可以发现 相关的数还是 f[j]+w, f[j-v]+2w, f[j-2v]+3w...
* 这就像和滑动窗口有点像了
*
* 不过滑动窗口里的值是不会变的 f[j]在更新后会发生改变 这就需要一个方法来表示固定的值:
* 原本:
* f[j] = f[j]
* f[j+v] = max(f[j]+ w, f[j+v])
* f[j+2v] = max(f[j]+2w, f[j+v]+w, f[j+2v])
* 改成:
* f[j] = f[j]
* f[j+v] = max(f[j], f[j+v]-w) + w
* f[j+2v] = max(f[j], f[j+v]-w, f[j+2v]-2w) + 2w
* 我们的f[]仍然表示最优解 并且也方便单调队列的内部排序
*
* 根据 f[j+kv]-kw (也许可以叫做初始值) 来排最大值 最大值放在队首
* */
return 0;
}
混合背包
// n=1000 m=1000 v,w,s=1000
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 10;
int n, m, K;
struct Thing {
int v, w, s;
};
vector<Thing> things;
int f[N];
int main() {
ios::sync_with_stdio(0);
cin >> n >> m; // 个数 最大容量
for (int i = 1, v, w, s; i <= n; ++i) {
cin >> v >> w >> s;// 体积 价值 个数
if (s == -1 || s == 0)
things.push_back({v, w, s}); //题目要求 s=-1 01背包 s=0 完全背包
else {
//二进制优化
for (int k = 1; k <= s; k *= 2) {
s -= k;
things.push_back({v * k, w * k, -1}); //多重背包可以用01背包来做
}
if (s) things.push_back({v * s, w * s, -1});
}
}
for (auto thing:things) {
if (thing.s == -1) {//01背包
for (int j = m; j >= thing.v; --j) {
f[j] = max(f[j], f[j - thing.v] + thing.w);
}
} else {//完全背包
for (int j = thing.v; j <= m; j++) {
f[j] = max(f[j], f[j - thing.v] + thing.w);
}
}
}
cout << f[m] << endl;
return 0;
}
二维费用的背包问题
#include <bits/stdc++.h>
using namespace std;
const int N = 1e2 + 10;
int n, M, V;
int f[N][N];
int main() {
ios::sync_with_stdio(0);
cin >> n >> V >> M;
for (int i = 1, v, w, m; i <= n; ++i) {
cin >> v >> m >> w;
for (int j = V; j >= v; --j) {
for (int k = M; k >= m; --k) {
f[j][k] = max(f[j][k], f[j - v][k - m] + w);
}
}
}
cout << f[V][M] << endl;
return 0;
}
分组背包
每组物品有s种,同一组内的物品最多只能选一个
f
i
,
j
f_{i,j}
fi,j 表示第
i
i
i 组在体积为
j
j
j 的情况下的最大价值
//核心代码
// f[i][j]表示第i组在体积为j的情况下的最大价值
// 可以优化为一维f[j]
for (int i = 1; i <= n; ++i) {
for (int j = V; j >= 0; --j) {
f[j] = max(f[j], f[j - v[1]] + w[1], f[j - v[2]] + w[2], ..., f[j - v[s]] + w[s]);
}
}
// 与多重背包有些相似
// 多重背包是分组背包的一种特殊情况 特殊情况才有特殊的方法 分组背包只能循环
分组背包板子
#include <bits/stdc++.h>
using namespace std;
const int N = 1e3 + 10;
int n, m, K;
int f[N];
int w[N], v[N];
int main() {
ios::sync_with_stdio(0);
cin >> n >> m;
for (int i = 1, s; i <= n; ++i) {
cin >> s;
for (int j = 1; j <= s; ++j) {
cin >> v[j] >> w[j];
}
for (int j = m; j >= 0; --j) {
for (int k = 1; k <= s; ++k) {
if (j >= v[k])
f[j] = max(f[j], f[j - v[k]] + w[k]);
}
}
}
cout << f[m] << endl;
return 0;
}
*难
有依赖的背包问题
背包 + 树形dp
f i , j f_{i,j} fi,j 表示选节点 i i i 体积是 j j j 的情况下整颗子树的最大价值
// n=100 m=100 v,w=100
#include <bits/stdc++.h>
using namespace std;
const int N = 1e3 + 10;
int n, m, root;
int f[N][N];
// f[i][j]表示选节点i体积是j的情况下整颗 子树 的最大价值
// 与分组背包 f[i][j]每组i在体积为j的情况下的最大价值 类似
// 所以有依赖背包其实就是分组背包的变形
vector<int> e[N];//edge
int w[N], v[N];
void dfs(int u) {
//先循环物品 再循环体积 最后循环决策
for (int son:e[u]) {
dfs(son);
// 先更新儿子传上来的值
// 此时u节点本身还没有加入进去 但是需要把u的位置空出来
// 所以总体积从m-v[u]开始
for (int j = m - v[u]; j >= 0; --j) {
for (int k = 0; k <= j; ++k) {
f[u][j] = max(f[u][j], f[u][j - k] + f[son][k]);
}
}
}
// 然后把u的值加进去
for (int i = m; i >= v[u]; --i) {
f[u][i] = f[u][i - v[u]] + w[u];
}
// 这一部分是没有加入u的 由于u的儿子依赖u
// 如果u不选 儿子肯定不选 所以这一部分的f[][]直接清零
for (int i = 0; i < v[u]; ++i) {
f[u][i] = 0;
}
}
int main() {
ios::sync_with_stdio(0);
cin >> n >> m;
for (int i = 1, p; i <= n; ++i) {
cin >> v[i] >> w[i] >> p;
if (p == -1)root = i;
else {
e[p].push_back(i);
}
}
dfs(root);
cout << f[root][m] << endl;
return 0;
}