背包问题
背包问题
经验
所有从i-1层转移过来的都要从大到小循环 所有从i层转移过来的都要从小到大循环
总结:所有背包问题优化成一维之后 只有完全背包问题和多重背包单调队列优化体积是从小到大循环的
for 物品
for 体积
for 决策
不超过:
f = 0 保证j - v >= 0
恰好:
f[0][0][0] = 0, f[0][j][k] = (+-)0x3f3f3f3f\0; 保证j - v >= 0 // f的取值具体情况具体分析
至少
f[0][0][0] = 0, f[0][j][k] = (+-)0x3f3f3f3f; max(0, j - v) // 负数的时候也要考虑 万一价值更加合适呢
01背包
二维模板
#include <iostream>
using namespace std;
int const N = 1010;
int f[N][N];
int main()
{
int n, m;
cin >> n >> m;
for(int i = 1; i <= n; i ++)
{
int x, w;
cin >> x >> w;
for(int j = 0; j <= m; j++)
{
f[i][j] = f[i - 1][j]; // 不选
if(j >= x) f[i][j] = max(f[i][j], f[i - 1][j - x] + w);
}
}
cout << f[n][m] << endl;
return 0;
}
一维模板 j从m到v
#include <iostream>
using namespace std;
int const N = 1000010;
int f[N];
int main()
{
int n, m;
cin >> n >> m;
int s, w;
for (int i = 1; i <= n; i++)
{
cin >> s >> w;
for (int j = m; j >= s; j--)
f[j] = max(f[j], f[j - s] + w);
}
cout << f[m] << endl;
return 0;
}
AcWing 734. 能量石
- 先贪心!
假定吃的时候i和i + 1这两块石头的能量都大于0 则获得的能量为Ei + Ej - Si * Lj(这里的E表示的是吃第一块的一瞬间两块石头剩余的能量)
换过来呢就是Ej + Ei - Sj * Li什么情况下第一种更好呢?Si * Lj < Sj * Li
即Si / Li < Sj / Lj按照这种情况排序 - 01背包!
- 状态表示
f[i][j]- 集合 所有从前i个物品中选,且总体积恰好是j的方案 // 体积是时间 之所以是恰好 是因为时间不是越大越好 能量石的能量会随时间流逝
- 属性 Max
- 状态计算
f[i][j] = max(f[i - 1][j], f[i - 1][j - s] + e - (j - s) * L)
- 状态表示
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
int const N = 110, M = 10010;
int f[M];
struct Node
{
int s, e, l;
bool operator < (const Node & node) const
{
return s * node.l < node.s * l;
}
}tr[N];
int main()
{
int T;
cin >> T;
for(int C = 1; C <= T; C ++)
{
int n, m = 0;
cin >> n;
for(int i = 1; i <= n; i ++)
{
int s, e, l;
cin >> s >> e >> l; // time get lost
m += s;
tr[i] = {s, e, l};
}
sort(tr + 1, tr + n + 1);
memset(f, -0x3f, sizeof f);
f[0] = 0;
for(int i = 1; i <= n; i ++)
{
int s = tr[i].s, e = tr[i].e, l = tr[i].l;
for(int j = m; j >= s; j --)
f[j] = max(f[j], f[j - s] + e - (j - s) * l);
}
int res = 0;
for(int j = 1; j <= m; j ++) res = max(res, f[j]);
printf("Case #%d: %d\n", C, res);
}
return 0;
}
完全背包
二维优化模板
求所有前缀的最大值
#include <iostream>
using namespace std;
int const N = 1010;
int f[N][N];
int main()
{
int n, m;
cin >> n >> m;
for (int i = 1; i <= n; i++)
{
int v, w;
cin >> v >> w;
for(int j = 0; j <= m; j ++)
{
f[i][j] = f[i - 1][j];
if(j >= v)
f[i][j] = max(f[i][j], f[i][j - v] + w);
}
}
cout << f[n][m];
return 0;
}
一维优化模板 j从v到m
#include <iostream>
using namespace std;
int const N = 1010;
int f[N];
int main()
{
int n, m;
cin >> n >> m;
for (int i = 1; i <= n; i++)
{
int v, w;
cin >> v >> w;
for(int j = v; j <= m; j ++)
f[j] = max(f[j], f[j - v] + w);
}
cout << f[m];
return 0;
}
AcWing 532. 货币系统
性质1 a1 a2 .. an 一定可以表示出来
性质2 在最优解中 b1 b2 .. bm 一定都是从a1 a2 .. an 中选择
性质3 b1 b2 .. bm 一定不能被其他 bi表示
以此从小到大枚举每一个数 若该数能被之前的凑出,则`continue`
简单来说就是完全背包计算能否恰好凑出体积m
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
int const N = 25010, M = 110;
int f[N], a[M], n;
int main()
{
int t;
cin >> t;
while (t--)
{
memset(f, 0, sizeof f);
f[0] = 1;
cin >> n;
for (int i = 1; i <= n; i++) cin >> a[i];
sort(a + 1, a + 1 + n);
int ans = 0;
for (int i = 1; i <= n; i++)
{
if (f[a[i]]) continue;
ans++;
for (int j = a[i]; j < N; j++)
f[j] |= f[j - a[i]];
}
cout << ans << endl;
}
return 0;
}
多重背包
多重背包Ⅰ
二维
#include <iostream>
using namespace std;
int const N = 110;
int f[N][N];
int main()
{
int n, m;
cin >> n >> m;
for (int i = 1; i <= n; i ++)
{
int v, w, s;
cin >> v >> w >> s;
for (int j = 0; j <= m; j ++)
for(int k = 0; k <= s && j >= k * v; k ++) // k == 0 的时候就是不选的情况 因此不需要特殊处理f[i][j] = f[i - 1][j]
f[i][j] = max(f[i][j], f[i - 1][j - k * v] + k * w);
}
cout << f[n][m];
return 0;
}
一维 j从m到0
#include <iostream>
using namespace std;
int const N = 110;
int f[N];
int main()
{
int n, m;
cin >> n >> m;
for (int i = 1; i <= n; i ++)
{
int v, w, s;
cin >> v >> w >> s;
for (int j = m; j >= v; j --)
for(int k = 0; k <= s && j >= k * v; k ++)
f[j] = max(f[j], f[j - k * v] + k * w);
}
cout << f[m];
return 0;
}
多重背包Ⅱ 二进制
#include <iostream>
using namespace std;
int const N = 1000010;
int f[N];
int w[N], v[N];
int cnt;
int main()
{
int n, m;
cin >> n >> m;
while(n --)
{
int a, b, c;
cin >> a >> b >> c; // v w s
int k = 1;
while(c >= k)
{
cnt ++;
c -= k;
v[cnt] = k * a;
w[cnt] = k * b;
k *= 2;
}
if(c)
{
cnt ++;
v[cnt] = c * a;
w[cnt] = c * b;
}
}
n = cnt;
for(int i = 1; i <= n; i ++)
for(int j = m; j >= v[i]; j--)
f[j] = max(f[j], f[j - v[i]] + w[i]);
cout << f[m] << endl;
return 0;
}
多重背包Ⅲ 单调队列 -> 滑动窗口求最值
求滑动窗口内的最大值 题解
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 20010;
int n, m;
int f[N], g[N], q[N];
int main()
{
cin >> n >> m;
for (int i = 0; i < n; i ++ )
{
int v, w, s;
cin >> v >> w >> s;
memcpy(g, f, sizeof f);
for (int j = 0; j < v; j ++ )
{
int hh = 0, tt = -1;
for (int k = j; k <= m; k += v)
{
if (hh <= tt && q[hh] < k - s * v) hh ++ ;
while (hh <= tt && g[q[tt]] - (q[tt] - j) / v * w <= g[k] - (k - j) / v * w) tt -- ;
q[ ++ tt] = k;
f[k] = g[q[hh]] + (k - q[hh]) / v * w;
}
}
}
cout << f[m] << endl;
return 0;
}
分组背包
模板
二维
#include <iostream>
using namespace std;
int const N = 110;
int s[N], w[N][N], v[N][N], f[N][N];
int main()
{
int n, m;
cin >> n >> m;
for(int i = 1; i <= n; i ++)
{
cin >> s[i];
for(int j = 1; j <= s[i]; j ++)
cin >> v[i][j] >> w[i][j];
}
for(int i = 1;i <= n; i ++)
for(int j = 0; j <= m; j ++)
{
f[i][j] = f[i - 1][j]; // 注意这个要放在循环外 若放在内层循环会改变最大值
for(int k = 1; k <= s[i]; k ++)
{
if(j >= v[i][k])
f[i][j] = max(f[i][j], f[i - 1][j - v[i][k]] + w[i][k]);
}
}
cout << f[n][m];
return 0;
}
一维
#include <iostream>
using namespace std;
int const N = 110;
int s[N], w[N][N], v[N][N], f[N];
int main()
{
int n, m;
cin >> n >> m;
for(int i = 1; i <= n; i ++)
{
cin >> s[i];
for(int j = 1; j <= s[i]; j ++)
cin >> v[i][j] >> w[i][j];
}
for(int i = 1;i <= n; i ++)
for(int j = m; j >= 0 ; j --)
for(int k = 1; k <= s[i]; k ++)
if(j >= v[i][k])
f[j] = max(f[j], f[j - v[i][k]] + w[i][k]);
cout << f[m];
return 0;
}
AcWing 487. 金明的预算方案
有依赖背包+分组背包
可以将每个主件及其附件看作一个物品组,记主件为 p,两个附件为a,b,则最多一共有4种组合:
- p
- p,a
- p,b
- p,a,b
这四种组合是互斥的,最多只能从中选一种,因此可以将每种组合看作一个物品,那么问题就变成了分组背包问题。可以参考AcWing 9. 分组背包问题。
在枚举四种组合时可以使用二进制的思想,可以简化代码。
#include <iostream>
#include <vector>
using namespace std;
typedef pair<int, int> PII;
int const N = 70, M = 32010;
PII master[N];
vector<PII> servent[N];
int f[M];
int main()
{
int n, m;
cin >> n >> m;
for(int i = 1; i <= m; i ++)
{
//价格,p表示该物品的重要度(1~5),q表示该物品是主件还是附件。
int v, p, q;
cin >> v >> p >> q;
if(!q) master[i] = {v, v * p};
else servent[q].push_back({v, v * p});
}
for(int i = 1; i <= m; i ++)
if(master[i].first)
{
for(int j = n; j >= 0; j --)
{
for(int k = 0; k < 1 << servent[i].size(); k ++)
{
int v = master[i].first, w = master[i].second;
for(int u = 0; u < servent[i].size(); u ++)
{
if(k >> u & 1)
{
v += servent[i][u].first;
w += servent[i][u].second;
}
}
if(j >= v) f[j] = max(f[j], f[j - v] + w);
}
}
}
cout << f[n];
return 0;
}
二维费用背包
分析
- 状态表示
f[i][j][k]- 集合 所有只从前i个物品中选,并且
费用1不超过j,费用2不超过k的选法 - 属性
Max
- 集合 所有只从前i个物品中选,并且
- 状态计算
- 所有不包含物品的选法
f[i - 1][j][k] - 所有 包含物品的选法
f[i - 1][j - v1][k - v2] + w
- 所有不包含物品的选法
模板
#include <iostream>
using namespace std;
int const N = 110;
int f[N][N];
int main()
{
int n, V, M;
cin >> n >> V >> M; // 物品数 体积 重量
for(int i = 1; i <= n; i ++)
{
int v, m, w;
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];
return 0;
}
AcWing 1022. 宠物小精灵之收服
二维费用背包问题
花费1 精灵球的数量
花费2 皮卡丘的体力值
价值 小精灵的数量
f[i][j][k]所有只从前i个物品中选 花费1不超过j 花费2不超过k的选法的最大价值
f[i][j][k] = max(f[i - 1][j][j], f[i - 1][j - v1[i]][k - v2[i]] + (w[i] == 1))
最多收服的小精灵数量 f[K][N][M]
最少耗费体力 f[K][N][m] == f[K][N][M]
#include <iostream>
using namespace std;
//f[i][j][k]
int const N = 1010;
int const M = 510;
int f[N][M];
int main()
{
int V1, V2, n; // 小智的精灵球数量、皮卡丘初始的体力值
cin >> V1 >> V2 >> n;
for(int i = 0; i < n; i ++)
{
int v1, v2, w = 1;
cin >> v1 >> v2;
for(int j = V1; j >= v1; j --)
for(int k = V2 - 1; k >= v2; k --) // 体力值至少为1
f[j][k] = max(f[j][k], f[j - v1][k - v2] + w);
}
cout << f[V1][V2 - 1] << ' '; //
for(int i = 0; ; i ++)
if(f[V1][i] == f[V1][V2 - 1])
{
cout << V2 - i << endl;
break;
}
return 0;
}
AcWing 1020. 潜水员
所有只从前面i个物品中选,并且总体积至少是j,总重量至少是k的选法
Min
#include <iostream>
#include <cstring>
using namespace std;
int f[30][90];
int main()
{
int N, M, K; //氧 氮 气缸的个数
cin >> N >> M >> K;
memset(f, 0x3f, sizeof f);
f[0][0] = 0;
for(int i = 1; i <= K; i ++)
{
int a, b, c;
cin >> a >> b >> c; //气缸里的氧和氮的容量及气缸重量。
for(int j = 21; j >= 0; j --)
for(int k = 79; k >= 0; k --)
f[j][k] = min(f[j][k], f[max(0, j - a)][max(0, k - b)] + c);
}
cout << f[N][M];
return 0;
}
方案数
经验
typedef lonh long LL;
AcWing 278. 数字组合
01背包求方案数(装满背包的方案书)
恰好 但是非法方案f就是等于0
f[0] = 1; f[j] += f[j - v];
AcWing 1021. 货币系统
完全背包求方案数(装满背包的方案书)
恰好 注意初始化 f[0] = 1 // 从前i个物品中选 体积恰好是0 因此只有1中方案就是啥都不选
else f = 0
AcWing 11. 背包问题求方案数
本题是求最优解(价值最大)的方案数
等价于求最短路径的条数
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
int const N = 1010, mod = 1e9 + 7;
int f[N], g[N]; // 恰好 f价值 g方案
//f[i]表示体积恰好为i的总价值 g[i]表示体积恰好为j价值最大的方案数
int main()
{
int n, m;
cin >> n >> m;
memset(f, -0x3f, sizeof f); //恰好的初始化方式
f[0] = 0; // 啥也不装 价值为0
g[0] = 1; // 体积为0 只有啥都不选这一种方案
for(int i = 1; i <= n; i ++)
{
int v, w;
cin >> v >> w;
for(int j = m; j >= v; j --)
{
int maxv = max(f[j], f[j - v] + w);
int cnt = 0;
if(f[j] == maxv) cnt = (cnt + g[j]) % mod; // 大概就类似求最短路径的条数 看看是否能从f[i-1][j]转移过来 若可以 则加上该方案 下面f[i-1][j-v]同理
if(f[j - v] + w == maxv) cnt = (cnt + g[j - v]) % mod;
g[j] = cnt; f[j] = maxv;
}
}
int res = 0, cnt = 0;
// 找到最大价值 因为f表示的是恰好 f[m不一定是最大价值]
for(int j = 0; j <= m; j ++) res = max(res, f[j]);
// 把所有最大价值的 方案都加起来
for(int j = 0; j <= m; j ++)
if(f[j] == res)
cnt = (cnt + g[j]) % mod;
cout << cnt << endl;
return 0;
}
背包问题求具体方案
konw
所谓求具体方案其实就是判断出每个物品是否被选,对应的是: 最短路问题求最短路径
以01背包为例子
f[i][j] = max(f[i - 1][j], f[i - 1][j - v[i]] + w[i])
f[n][m]
本质是求从哪条路劲走到f[n][m]
为了保证字典序最小,采用贪心策略:从小到大对于每一个物品由3种情况
1. 只能选 选!
2. 不能选 不选!
3. 可选可不选 选!
但是我们推出方案是从后往前得到的,和贪心策略相反。怎么办呢?
只需求解01背包的时候从后往前推即可
模板
#include <iostream>
using namespace std;
int const N = 1010;
int f[N][N];
int v[N], w[N];
int main()
{
int n, m;
cin >> n >> m;
for(int i = 1; i <= n; i ++) cin >> v[i] >> w[i]; // 注意输入问题
for(int i = n; i >= 1; i --)
{
for(int j = 0; j <= m; j ++)
{
f[i][j] = f[i + 1][j];
if(j >= v[i])
f[i][j] = max(f[i][j], f[i + 1][j - v[i]] + w[i]);
}
}
// f[1][m]为最大价值
// 从后往前推
int j = m;
for(int i = 1; i <= n; i ++)
{
if(j >= v[i] && f[i][j] == f[i + 1][j - v[i]] + w[i]) // 可选 则选
{
cout << i << ' ';
j -= v[i];
}
}
return 0;
}
AcWing 1013. 机器分配
分组背包问题
分析:把每个公司当成一个物品组
例如公司1有3个物品(v,w)-{(1, 30)(2, 40)(3, 50)}
M表示体积 求最大价值
综上所述:分组背包模板
#include <iostream>
#include <map>
using namespace std;
typedef pair<int, int> PII;
int const N = 110;
int f[N][N], v[N][N], w[N][N];
map<int, int> mp;
int main()
{
int n, m;
cin >> n >> m;
for(int i = 1; i <= n; i ++)
for(int j = 1; j <= m; j ++)
{
cin >> w[i][j];
v[i][j] = j;
}
for(int i = 1; i <= n; i ++)
for(int j = 0; j <= m; j ++)
{
f[i][j] = f[i - 1][j];
for(int k = 1; k <= m; k ++)
{
if(j >= v[i][k])
f[i][j] = max(f[i][j], f[i - 1][j - v[i][k]] + w[i][k]);
}
}
cout << f[n][m] << endl;
int j = m;
for(int i = 1; i <= n; i ++) mp[i] = 0;
for(int i = n; i >= 1; i --) // 注意从后往前推答案
for(int k = 1; k <= m; k ++)
if(j >= v[i][k] && f[i][j] == f[i - 1][j - v[i][k]] + w[i][k])
{
mp[i] = k;
j -= v[i][k];
break;
}
for(auto [u, v] : mp) cout << u << ' ' << v << endl;
return 0;
}
混合背包
模板
AcWing 7. 混合背包问题
分类讨论即可
#include <iostream>
using namespace std;
int const N = 1010;
int f[N];
int main()
{
int n, m;
cin >> n >> m;
while(n--)
{
int v, w, s;
cin >> v >> w >> s;
if(!s)
{
for(int j = v; j <= m; j ++) f[j] = max(f[j], f[j - v] + w);
}
else
{
if(s == -1) s = 1;
for(int k = 1; k <= s; k *= 2)
{
for(int j = m; j >= k * v; j --)
f[j] = max(f[j], f[j - k * v] + k * w);
s -= k;
}
if(s)
{
for(int j = m; j >= s * v; j --)
f[j] = max(f[j], f[j - s * v] + s * w);
}
}
}
cout << f[m];
return 0;
}
有依赖背包
前置知识树形dp
没有上司的舞会
f[i][0]表示以i为根的子树,不选第i个点的情况下,选出来的最大价值是多少
f[i][1]表示以i为根的子树, 选第i个点的情况下,选出来的最大价值是多少
状态转移
f[i][0] += max(f[j][0], f[j][1]) for(j : i的所有子树)
f[i][1] += f[j][0] for(j : i的所有子树) // i选了 子树全都不能选啦
最终答案 ans == max(f[i][0], f[i][1])
#include <iostream>
#include <cstring>
using namespace std;
int const N = 6010;
int e[N], ne[N], h[N], idx;
int f[N][2], w[N], st[N];
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
void dfs(int u)
{
// f[u][0] = 0; 最坏的情况下可以啥都不选 因此直接设置为0即可
f[u][1] = w[u];
for(int i = h[u]; ~i; i = ne[i])
{
int j = e[i];
dfs(j);
f[u][0] += max(f[j][1], f[j][0]);
f[u][1] += f[j][0];
}
}
int main()
{
memset(h, -1, sizeof h);
int n;
cin >> n;
for(int i = 1; i <= n; i ++) cin >> w[i];
for(int i = 1; i < n; i ++)
{
int a, b;
cin >> a >> b;
add(b, a);
st[a] = 1; // 是否存在父节点 标记一下 一遍搜寻根节点
}
int root = 1;
while(st[root]) root ++;
dfs(root); // 从根节点开始树形DP
cout << max(f[root][1], f[root][0]);
return 0;
}
AcWing 10. 有依赖的背包问题
目标 f[u][j]
1. 集合 所有从以u为根的子树中选,且总体积不超过j的方案 - 一维dp的简单拓展
2. 属性 Max
集合划分 划分为从分别子树中选择 每一颗子树内部按体积划分(0 ~ m)
#include <iostream>
#include <cstring>
using namespace std;
int const N = 110, M = N * N;
int v[N], w[N];
int e[N], ne[N], idx, h[N];
int f[N][M];
int n, m;
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx ++;
}
void dfs(int u)
{
for(int i = h[u]; ~i; i = ne[i]) // 循环物品
{
int son = e[i];
dfs(son);
for(int j = m - v[u]; j >= 0; j --) // 多重背包 从大到小 循环体积
for(int k = 0; k <= j; k ++) // 循环决策 将每棵树按照分配体积分成0~m
f[u][j] = max(f[u][j], f[u][j - k] + f[son][k]);
}
// 将u加进去
for(int j = m; j >= v[u]; j --) f[u][j] = f[u][j - v[u]] + w[u];
for(int j = 0; j < v[u]; j ++) f[u][j] = 0; // 当体积小于v[u]的时候 一定放不进去
}
int main()
{
memset(h, -1, sizeof h);
int root;
cin >> n >> m;
for(int i = 1; i <= n; i ++)
{
int p;
cin >> v[i] >> w[i] >> p;
if(p == -1) root = i;
else add(p, i);
}
dfs(root);
cout << f[root][m];
return 0;
}

浙公网安备 33010602011771号