Loading

「学习笔记」DP学习笔记 3

背包DP

0-1背包

给你 \(n\) 个物品和一个容量为 \(W\) 的背包,每个物品有自己的价值 \(v\) 和 需要占用的空间 \(c\),求背包中的物品的所占用的空间不超过容量的最大价值。

特点:每个物品只能选一次

设置状态:\(f(i, j)\) 意味着前 \(i\) 个物品,容量为 \(j\) 的最大价值。

对于第 \(i\) 个物品,有选和不选两种情况,由此可以得到状态转移方程:

\[f(i, j) = \max \{ f(i - 1, j), f(i - 1, j - c_i) + w_i \} \]

由于二维空间有时会 MLE,同时我们发现对于 \(f_i\),只有 \(f_{i - 1}\) 会对它有贡献,因此我们可以使用滚动数组优化,将状态转移方程优化为:

\[f_j = \max \{ f_j, f_{j - c_i} + w_i \} \]

写成该转移方程式,则枚举容量要从大到小枚举,因为在 \(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) = \max_{k = 0}^{+\infty} \{ f(i - 1, j), f(i - 1, j - k \times c_i) + v_i \times k \} \]

我们做一个简单的优化,对于 \(f(i, j)\),只要通过 \(f(i, j - c_i)\) 转移就行了,因此转移方程为:

\[f(i, j) = \max \{ f(i - 1, j), f(i, j - c_i) + v_i \} \]

为什么是对的呢,我们可以这样想,\(f(i, j - c_i)\) 已经由 \(f(i, j - 2 \times c_i)\) 更新过了,所以 \(f(i, j - c_i)\) 肯定是考虑了第 \(i\) 件物品的选择次数和空间的最优结果了,相当于最优子结构,我们可以利用局部最优子结构来优化枚举的复杂度。

同样,完全背包也可以将状态从二维化简为一维,转移方程式如下:

\[f_j = \max\{ f_j, f_{j - c_i} + v_i \} \]

枚举顺序为正序枚举。

P1616 疯狂的采药

#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\) 个。

\[f(i, j) = \max_{k = 0}^{k_i} \{ f(i - 1, j - k \times c_i) + v_i \times 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\) 次。

P1833 樱花

#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 了。

P1855 榨取kkksc03

#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 背包即可。

P1757 通天之分组背包

#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;
}

有依赖的背包

将主件与复件分类讨论,变化成分组背包即可。

P1064 [NOIP2006 提高组] 金明的预算方案

#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;
}
posted @ 2023-11-02 20:43  yi_fan0305  阅读(13)  评论(0编辑  收藏  举报