01 背包学习笔记升级版

· · 算法·理论

众所周知,一篇专栏也需要一个头图。

于是,本文出现了。这里有一个建议搭配使用的比赛。邀请码为 zli5

\texttt{Part 1 01} 背包模板

[题目传送门在这里。](https://www.luogu.com.cn/problem/U523050) - 确定**状态**:$dp_{i,j}$ 表示前面 $i$ 件物品,任意挑选装入背包**容量**为 $j$ 的背包之中可以获取的**最大价值**。$j$ 就是**背包的容量**,整个背包**可以不装满**。所以这就是**部分背包问题**。 - 确定**答案**:易得其为 $dp_{n,m}$。 - 确定**状态转移方程**:对于第 $i$ 件物品,一共**两种情况**。 - **第一种**:准备装入: - 当**装得下**时:$dp_{i,j}=dp_{i-1,j-w_i}+val_i$。 - 当**装不下**时:$dp_{i,j}=dp_{i-1,j}$。 - **第二种**:不准备装入: - $dp_{i,j}=dp_{i-1,j}$。 - **合并**之后可以发现: - $$dp_{i,j}=\max(dp_{i-1,j},dp_{i-1,j-w_i}+val_i)$$。 - **初始化**和**边界**:**全部**为 $0$。 代码实现如下: ```cpp #include<bits/stdc++.h> #define int long long using namespace std; const int maxn = 1e3 + 5; int n, m, w[maxn], val[maxn]; int dp[maxn][maxn]; signed main() { ios::sync_with_stdio(0); cin.tie(0), cout.tie(0); cin >> m >> n; for (int i = 1; i <= n; i++) { cin >> w[i] >> val[i]; } for (int i = 1; i <= n; i++) { for (int j = 0; j <= m; j++) { dp[i][j] = dp[i - 1][j]; if (j >= w[i]) dp[i][j] = max(dp[i][j], dp[i - 1][j - w[i]] + val[i]); } } cout << dp[n][m] << endl; return 0; } ``` 但是,有些毒瘤的出题人会卡我们的空间,这个时候,我们就需要进行**滚动数组优化空间**。 我们注意到,每一次的状态转移中,只用到了 `dp[i - 1][j]` 和 `dp[i - 1][j - w[i]]` 两个前面的量,而且关于 $i$ 的下标都是 $i-1$。所以,我们可以另外开一个数组 $pre$,维护所有的 $dp_{i-1,j}\ (0 \le j \le m)$ 的值,这样转移方程就可以为: $$dp_j = \begin{cases} pre_j & j < w_i \\ \max(pre_j,pre_{j-w_i}+val_i) & j \ge w_i \end{cases}$$ 但是,这样我们就需要在每一次的转移方程之后: ```cpp for (int j = 0; j <= m; j++) pre[j] = dp[j]; ``` 这样是为了存储当前的 $dp_j$ 的值,以便下一次转移的时候使用。这就是**滚动数组**的使用方式。所以,本题优化后的完整代码如下: ```cpp #include<bits/stdc++.h> #define int long long using namespace std; const int maxn = 1e3 + 5; int n, m, w[maxn], val[maxn]; int dp[maxn]; int pre[maxn]; signed main() { ios::sync_with_stdio(0); cin.tie(0), cout.tie(0); cin >> m >> n; for (int i = 1; i <= n; i++) { cin >> w[i] >> val[i]; } for (int i = 1; i <= n; i++) { for (int j = 0; j <= m; j++) { dp[j] = pre[j]; if (j >= w[i]) dp[j] = max(dp[j], pre[j - w[i]] + val[i]); } for (int j = 0; j <= m; j++) pre[j] = dp[j]; } cout << dp[m] << endl; return 0; } ``` 板子到此就讲完了。当你掌握了上面的内容之后,你就可以迅速地切掉这场比赛的前两题了。稍加思考之后,你也可以切掉第三题。 ### $\texttt{Part 2}$ 二维费用背包 实际上,二维费用背包就是有双重的费用,要满足两种费用都能够付出才能够购买这种物品。我们只需要将每一次的循环多开一维即可。但是还有一个重点,就是**初始化问题**。一般情况下,我们需要将其整体设为一个**极大值**或**极小值**,然后让 $dp_{0,0,0}=0$。但有的题目可能有些特殊,需要自行调整。 注意,**二维费用背包**也是可以使用**滚动数组优化**的。这样就可以将数组变为二维。当然,这也增加了**时间复杂度**。之前我们讲到,$\texttt{DP}$ 是用**空间换时间**,但是如果出题人要**卡空间**的话,那么我们就需要用**滚动数组优化**的方式用**时间换空间**。 来一道例题试试手。[洛谷 P1507](https://www.luogu.com.cn/problem/P1507) 有了前面的基础,我们很快就能打出如下的代码,注意滚动数组只能滚掉关于 $i$ 的一维,是不能滚掉关于**费用**维度的。所以,当我们做 $n$ 维费用背包的时候,原本需要开 $(n+1)$ 维的数组,但是经过**滚动数组**的优化之后,就只有 $2$ 个 $n$ 维的数组了。 ```cpp #include<bits/stdc++.h> #define int long long using namespace std; const int maxn = 4e2 + 5; const int INF = 2e9; int n, m, k, h[maxn], t[maxn], kl[maxn]; int dp[maxn][maxn]; int pre[maxn][maxn]; signed main() { ios::sync_with_stdio(0); cin.tie(0), cout.tie(0); cin >> n >> m >> k; for (int i = 1; i <= k; i++) { cin >> h[i] >> t[i] >> kl[i]; } memset(pre, INF, sizeof(pre)); pre[0][0] = 0; for (int i = 1; i <= k; i++) { for (int j = 0; j <= n; j++) { for (int l = 0; l <= m; l++) { dp[j][l] = pre[j][l]; if (j >= h[i] && l >= t[i]) dp[j][l] = max(dp[j][l], pre[j - h[i]][l - t[i]] + kl[i]); } } for (int j = 0; j <= n; j++) { for (int l = 0; l <= m; l++) { pre[j][l] = dp[j][l]; } } } cout << dp[n][m] << endl; return 0; } ``` 掌握了二维费用背包之后,你可以切掉这场比赛的第五题和第六题。实际上 T7 是一个较难的一维费用,多加思考之后也可以切掉。 ### $\texttt{Part 3}$ 存在性问题 实际上存在性问题比较简单,就是要满足一些 $\texttt{DP}$ 的基本条件,满足就标为 $1$,不满足就为 $0$。实在不会就去 [OI-wiki](https://oi.wiki/dp/knapsack/) 里面找吧。 这里找一道较难的例题来分析。[P1284 三角形牧场](https://www.luogu.com.cn/problem/P1284) ```#include<bits/stdc++.h> #define int long long using namespace std; const int maxn = 1605; int dp[maxn][maxn]; int pre[maxn][maxn]; int l[45]; /* 1.确定状态 dp[j][k] 表示前面 i 块木板,是否可以组装出来 2.确定答案 dp[j][k] 中找最大的三角形面积,具体看代码 3.状态转移方程 两个都装得下: dp[j][k] |= pre[j - l][k] | pre[j][k - l] 一个装得下: dp[j][k] |= pre[j - l][k] 另一个装得下: dp[j][k] |= pre[j][k - l] 装不下: dp[j][k] |= 0 4.初始值和边界 pre[0][0] = 1,其余为 0 */ int mj(int x, int y, int z) { double p = (x + y + z) / 2.0; double s = sqrt(p * (p - x) * (p - y) * (p - z)); return 100 * s; } bool pd(int a, int b, int c) { return a + b > c && a + c > b && a < b + c; } signed main() { ios::sync_with_stdio(0); cin.tie(0), cout.tie(0); int n; cin >> n; int sum = 0; for (int i = 1; i <= n; i++) { cin >> l[i]; sum += l[i]; } pre[0][0] = 1; for (int i = 1; i <= n; i++) { for (int j = 0; j <= sum; j++) { for (int k = 0; k <= sum; k++) { dp[j][k] = pre[j][k]; if (j >= l[i] && k >= l[i]) dp[j][k] |= pre[j - l[i]][k] | pre[j][k - l[i]]; else if (j >= l[i]) dp[j][k] |= pre[j - l[i]][k]; else if (k >= l[i]) dp[j][k] |= pre[j][k - l[i]]; else dp[j][k] |= 0; } } for (int j = 0; j <= sum; j++) { for (int k = 0; k <= sum; k++) { pre[j][k] = dp[j][k]; } } } int ans = -1; for (int j = 0; j <= sum; j++) { for (int k = 0; k <= sum; k++) { if (dp[j][k]) { int tmp = sum - j - k; if (pd(j, k, tmp)) ans = max(ans, mj(j, k, tmp)); } } } cout << ans << endl; return 0; } ``` 边打代码边写的注释,于是就不用 $\text{Markdown}$ 和 $\LaTeX$ 了。 弄懂了存在性问题,就可以把 T4 切掉,于是你 AK 了本场模拟赛,恭喜。 ![](https://cdn.luogu.com.cn/upload/image_hosting/4vgpv7hd.png)