「学习笔记」DP学习笔记 3
背包DP
0-1背包
给你 \(n\) 个物品和一个容量为 \(W\) 的背包,每个物品有自己的价值 \(v\) 和 需要占用的空间 \(c\),求背包中的物品的所占用的空间不超过容量的最大价值。
特点:每个物品只能选一次。
设置状态:\(f(i, j)\) 意味着前 \(i\) 个物品,容量为 \(j\) 的最大价值。
对于第 \(i\) 个物品,有选和不选两种情况,由此可以得到状态转移方程:
由于二维空间有时会 MLE,同时我们发现对于 \(f_i\),只有 \(f_{i - 1}\) 会对它有贡献,因此我们可以使用滚动数组优化,将状态转移方程优化为:
写成该转移方程式,则枚举容量要从大到小枚举,因为在 \(f_j\) 更新时,\(f_{j - c_i}\) 是不能被更新的,以免将 \(f(i - 1, j - c_i)\) 的信息覆盖掉。
例题:
[NOIP2005 普及组] 采药
山洞里有一些不同的草药,采每一株都需要一些时间,每一株也有它自身的价值。在给定的时间里,你要让采到的草药的总价值最大。
0-1 背包的模板题,很适合入门。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
template<typename T>
inline T read() {
T x = 0;
bool fg = 0;
char ch = getchar();
while (ch < '0' || ch > '9') {
fg |= (ch == '-');
ch = getchar();
}
while (ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + (ch ^ 48);
ch = getchar();
}
return fg ? -x : x;
}
const int N = 110;
int W, n;
int c[N], v[N], f[1010];
int main() {
W = read<int>(), n = read<int>();
for (int i = 1; i <= n; ++ i) {
c[i] = read<int>(), v[i] = read<int>();
}
for (int i = 1; i <= n; ++ i) {
for (int j = W; j >= c[i]; -- j) {
f[j] = max(f[j], f[j - c[i]] + v[i]);
}
}
printf("%d\n", f[W]);
return 0;
}
完全背包
与 0-1 背包类似,但不同的地方在于,0-1 背包中每种物品只能选一次,而完全背包中每种物品可以选无数次。
设置状态:\(f(i, j)\) 意味着前 \(i\) 个物品,容量为 \(j\) 的最大价值。
转移方程如下:
我们做一个简单的优化,对于 \(f(i, j)\),只要通过 \(f(i, j - c_i)\) 转移就行了,因此转移方程为:
为什么是对的呢,我们可以这样想,\(f(i, j - c_i)\) 已经由 \(f(i, j - 2 \times c_i)\) 更新过了,所以 \(f(i, j - c_i)\) 肯定是考虑了第 \(i\) 件物品的选择次数和空间的最优结果了,相当于最优子结构,我们可以利用局部最优子结构来优化枚举的复杂度。
同样,完全背包也可以将状态从二维化简为一维,转移方程式如下:
枚举顺序为正序枚举。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
template<typename T>
inline T read() {
T x = 0;
bool fg = 0;
char ch = getchar();
while (ch < '0' || ch > '9') {
fg |= (ch == '-');
ch = getchar();
}
while (ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + (ch ^ 48);
ch = getchar();
}
return fg ? -x : x;
}
const int N = 1e4 + 5;
const int M = 1e7 + 5;
int t, n;
int a[N], b[N];
ll f[M];
int main() {
t = read<int>(), n = read<int>();
for (int i = 1; i <= n; ++ i) {
a[i] = read<int>(), b[i] = read<int>();
}
for (int i = 1; i <= n; ++ i) {
for (int j = a[i]; j <= t; ++ j) {
f[j] = max(f[j], f[j - a[i]] + b[i]);
}
}
printf("%lld\n", f[t]);
return 0;
}
多重背包
与 0-1 背包的不同在于每个物品有 \(k\) 个。
优化:二进制拆分
可以使用二进制分组来时拆分方式更优美。
代码来自 \(\texttt{OI-Wiki}\)。
index = 0;
for (int i = 1; i <= m; i++) {
int c = 1, p, h, k;
cin >> p >> h >> k;
while (k > c) {
k -= c;
list[++index].w = c * p;
list[index].v = c * h;
c *= 2;
}
list[++index].w = p * k;
list[index].v = h * k;
}
混合背包
混合背包就是前三种背包混合在一起,即有些物品只能取一次,有些能取无数次,有些能取 \(k\) 次。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
template<typename T>
inline T read() {
T x = 0;
bool fg = 0;
char ch = getchar();
while (ch < '0' || ch > '9') {
fg |= (ch == '-');
ch = getchar();
}
while (ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + (ch ^ 48);
ch = getchar();
}
return fg ? -x : x;
}
const int N = 1e4 + 5;
using tii = tuple<int, int>;
int n, t;
int c[N], v[N], p[N];
ll f[N];
char s[N];
int main() {
int begh, begm, endh, endm;
scanf("%s", s);
sscanf(s, "%d:%d", &begh, &begm);
scanf("%s", s);
sscanf(s, "%d:%d", &endh, &endm);
t = (endm - begm + 60) % 60;
if (endm < begm) {
t += (endh - begh - 1) * 60;
} else {
t += (endh - begh) * 60;
}
n = read<int>();
for (int i = 1; i <= n; ++ i) {
c[i] = read<int>(), v[i] = read<int>(), p[i] = read<int>();
}
for (int i = 1; i <= n; ++ i) {
if (p[i] == 1) {
for (int j = t; j >= c[i]; -- j) {
f[j] = max(f[j], f[j - c[i]] + v[i]);
}
} else if (p[i] == 0) {
for (int j = c[i]; j <= t; ++ j) {
f[j] = max(f[j], f[j - c[i]] + v[i]);
}
} else {
vector<tii> tmp;
for (int g = 1; p[i] > g; g <<= 1) {
p[i] -= g;
tmp.emplace_back(g * c[i], g * v[i]);
}
if (p[i]) {
tmp.emplace_back(p[i] * c[i], p[i] * v[i]);
}
int C, V;
for (tii it : tmp) {
tie(C, V) = it;
for (int j = t; j >= C; -- j) {
f[j] = max(f[j], f[j - C] + V);
}
}
}
}
printf("%lld\n", f[t]);
return 0;
}
二维费用背包
与 0-1 背包相比,二维费用背包在选择物品时还要考虑费用,只需要在多一层循环来枚举费用即可,这里再开一维空间来存放物品编号就很容易 MLE 了。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
template<typename T>
inline T read() {
T x = 0;
bool fg = 0;
char ch = getchar();
while (ch < '0' || ch > '9') {
fg |= (ch == '-');
ch = getchar();
}
while (ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + (ch ^ 48);
ch = getchar();
}
return fg ? -x : x;
}
const int N = 110;
int n, m, t;
int mo[N], ti[N];
ll f[N << 1][N << 1];
int main() {
n = read<int>(), m = read<int>(), t = read<int>();
for (int i = 1; i <= n; ++ i) {
mo[i] = read<int>(), ti[i] = read<int>();
}
for (int i = 1; i <= n; ++ i) {
for (int j = m; j >= mo[i]; -- j) {
for (int k = t; k >= ti[i]; -- k) {
f[j][k] = max(f[j][k], f[j - mo[i]][k - ti[i]] + 1);
}
}
}
printf("%lld\n", f[m][t]);
return 0;
}
分组背包
与 0-1 背包相比,就是在当前组中只能选择一个,然后求最大价值。
对每一组都进行 0-1 背包即可。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
template<typename T>
inline T read() {
T x = 0;
bool fg = 0;
char ch = getchar();
while (ch < '0' || ch > '9') {
fg |= (ch == '-');
ch = getchar();
}
while (ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + (ch ^ 48);
ch = getchar();
}
return fg ? -x : x;
}
const int N = 1100;
using tii = tuple<int, int>;
int n, m, lim;
ll f[N];
vector<tii> g[N];
int main() {
m = read<int>(), n = read<int>();
for (int i = 1, a, b, c; i <= n; ++ i) {
a = read<int>(), b = read<int>(), c = read<int>();
g[c].emplace_back(a, b);
lim = max(lim, c);
}
for (int i = 1; i <= lim; ++ i) {
for (int j = m; j >= 0; -- j) {
for (tii it : g[i]) {
if (j >= get<0>(it)) {
f[j] = max(f[j], f[j - get<0>(it)] + get<1>(it));
}
}
}
}
printf("%lld\n", f[m]);
return 0;
}
有依赖的背包
将主件与复件分类讨论,变化成分组背包即可。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
template<typename T>
inline T read() {
T x = 0;
bool fg = 0;
char ch = getchar();
while (ch < '0' || ch > '9') {
fg |= (ch == '-');
ch = getchar();
}
while (ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + (ch ^ 48);
ch = getchar();
}
return fg ? -x : x;
}
const int N = 5e4 + 5;
using tii = tuple<int, int>;
int n, m;
ll sumv, sump;
ll v[N], p[N], q[N];
ll f[N];
vector<tii> s[N], z[N];
void dfs(int i, int u) {
if (u == (int)s[i].size()) {
z[i].emplace_back(v[i] + sumv, p[i] * v[i] + sump);
return ;
}
sumv += get<0>(s[i][u]);
sump += get<1>(s[i][u]) * get<0>(s[i][u]);
dfs(i, u + 1);
sumv -= get<0>(s[i][u]);
sump -= get<1>(s[i][u]) * get<0>(s[i][u]);
dfs(i, u + 1);
}
int main() {
n = read<int>(), m = read<int>();
for (int i = 1; i <= m; ++ i) {
v[i] = read<int>(), p[i] = read<int>(), q[i] = read<int>();
s[q[i]].emplace_back(v[i], p[i]);
}
for (int i = 1; i <= m; ++ i) {
if (q[i] == 0) {
dfs(i, 0);
}
}
for (int i = 1; i <= m; ++ i) {
for (int j = n; j >= 0; -- j) {
int V, P;
for (tii it : z[i]) {
tie(V, P) = it;
if (V <= j) {
f[j] = max(f[j], f[j - V] + P);
}
}
}
}
printf("%lld\n", f[n]);
return 0;
}