动态规划(一)
1.01背包
有 \(N\) 件物品和一个容量是 \(V\) 的背包。每件物品只能使用一次。
第 \(i\) 件物品的体积是 \(v_{i}\),价值是 $ w_{i}$。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。
思维导图:
状态表示:\(f[i][j]\) 表示从前 \(i\) 个物品中选出了总体积为 \(j\) 的物品放入背包,物品的最大价值和。
状态划分:其实是集合划分。
对于第 \(i\) 个物品,有两种策略,一种是选它,一种是不选它。所以我们可以将 \(f[i][j]\) 这个集合划分成两个集合,其中,不含 \(i\) 的集合就相当于 \(f[i - 1][j]\),含 \(i\) 的集合就相当于 \(f[i - 1][j - v[i]] + w[i]\) (先将第 \(i\) 个物品从背包中拿出来,再放回去,最大值仍是最大值)。
综上所述,状态转移方程为 \(f[i][j] = max(f[i - 1][j], f[i - 1][j - v[i]] + w[i])\)
代码如下:
#include <iostream>
using namespace std;
const int N = 1010;
int n, m;
int v[N], w[N], f[N][N];
int main() {
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; i++) {
scanf("%d%d", &v[i], &w[i]);
}
for(int i = 1; i <= n; 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 - 1][j], f[i - 1][j - v[i]] + w[i]);
}
}
printf("%d", f[n][m]);
return 0;
}
通过状态转移方程,我们发现,每一阶段 \(i\) 的状态只与上一阶段 \(i - 1\) 的状态有关。同时 \(j - v[i]\) 和 \(j\) 都是小于等于 \(j\) 的,所以可以用滚动数组的优化方式,将 \(f\) 数组优化成一维。
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]);
}
}
注意:由于 \(j - v[i]\) 严格小于 \(j\) 所以在每层循环中会先被计算,这样就会导致在计算 \(f[i][j]\) 时调用的 \(f[i - 1][j - v[i]]\) 已经被更新成了 \(f[i][j - v[i]]\) ,所以内层循环要从大到小。
2.完全背包
有 \(N\) 种物品和一个容量是 \(V\) 的背包,每种物品都有无限件可用。
第 \(i\) 种物品的体积是 \(v_{i}\),价值是 \(w_{i}\)。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。
思维导图:
集合划分:### 思维导图:
其实本质上还是可以用01背包的思想。
整理一下,得出状态转移方程:\(f[i][j] = max(f[i - 1][j], f[i - 1][j - v[i] * k] + w[i] * k)\)
代码如下:
#include <iostream>
using namespace std;
const int N = 1010;
int n, m;
int v[N], w[N], f[N][N];
int main() {
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; i++) {
scanf("%d%d", &v[i], &w[i]);
}
for(register int i = 1; i <= n; i++) {
for(register int j = 0; j <= m; j++) {
for(register int k = 0; k * v[i] <= j; k++) {
f[i][j] = max(f[i][j], f[i - 1][j - k * v[i]] + w[i] * k);
}
}
}
printf("%d", f[n][m]);
return 0;
}
但是我们会发现,这个代码的时间和空间复杂度太高,为 \(O(nms)\),我们来考虑优化。
将这个式子展开,会得到:
而展开之后可以发现,两个式子的中间项竟如此相似,而且上式就比下式多加了 \(w\) ,所以框起来的最大值也比下式多了 \(w\) 。
综上所述,动态转移方程可以简化为:\(f[i][j] = max(f[i - 1][j], f[i][j - v[i]] + w[i])\)
再像01背包一样去掉一维就得到:
\(f[j] = max(f[j], f[j - v[i]] + w[i])\)
最终代码:
for(int i = 1; i <= n; i++) {
for(int j = v[i]; j <= m; j++) {
f[j] = max(f[j], f[j - v[i]] + w[i]);
}
}
注意:完全背包和01背包的区别:
由于完全背包调用的是 \(f[i][j - v[i]]\), 所以应该正序循环!
3.多重背包
有 \(N\) 种物品和一个容量是 \(V\) 的背包。
第 \(i\) 种物品最多有 \(s_{i}\) 件,每件体积是 \(v_{i}\),价值是 \(w_{i}\)。
求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。
输出最大价值。
思维导图:
多重背包的朴素版本其实和完全背包极其相似,它的状态转移方程为:
\(f[i][j] = max(f[i - 1][j], f[i - 1][j - v[i] * s[i]] + w[i] * s[i])\)
同样的,我们来考虑优化。
通过尝试可以发现,优化完全背包的方法在这里行不通,因为多了一项。
那怎么办呢?
二进制优化法
由于一个一个地装太慢了,于是可以采用二进制拼凑的方法来拼出所有的可能。
众所周知,从 \(2^0,2^1,2^2,…,2^{k - 1}\) 这 \(k\) 个 \(2\) 的整数次幂中选出若干个数相加,可以表示出 \(0\) ∼ \(2^k - 1\) 之间的任何整数。我们求出满足 \(2^0 + 2^1 + 2^2 + … + 2^{p}\) 的最大整数 \(p\),设 \(R_{i} = S_{i} - 2^0 + 2^1 + 2^2 + … + 2^{p + 1}\),那么:
由于 \(2^0,2^1,2^2…,2^{p}\) 能拼凑出 \(0\) ∼ \(2^{p + 1} - 1\) 中的所有数,所以 \(2^0,2^1,2^2…,2^{p},R_{i}\) 能拼凑出 \(R_{i}\) ∼ \(2^{p + 1} + R_{i} - 1\) 的所有数,即 \(R_{i}\) ∼ \(S_{i}\) 的所有数。又因为 \(R_{i} < 2^{p + 1}\) ,所以两个区间可以合并变成 \(0\) ∼ \(S_{i}\),综上所述,我们可以将数量为 \(S_{i}\) 的第 \(i\) 个物品拆分成 \(p + 2\) 个物品,它们的体积分别为:\(2^0 * V_{i},2^1 * V_{i}\)\(,…,\)\(2^p * V_{i}\),\(2^{p + 1} * V_{i}\)
这相当于对这每一份物品进行一次01背包就行了,时间复杂度降到 \(O(nmlog s)\)。
代码如下:
#include <iostream>
using namespace std;
const int N = 2010, M = 25000;
int dp[N], v[M], w[M];
int n, m;
int main() {
scanf("%d%d", &n ,&m);
int cnt = 0;
for(int i = 1; i <= n; i++) {
int a, b, s;
scanf("%d%d%d", &a, &b, &s);
int k = 1;
while(k < s) {
cnt++;
v[cnt] = a * k;
w[cnt] = b * k;
s -= k;
k *= 2;
}
if(s > 0) {
cnt++;
v[cnt] = a * s;
w[cnt] = b * s;
}
}
n = cnt;
for(int i = 1; i <= n; i++) {
for(int j = m; j >= v[i]; j--) {
dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
}
}
printf("%d\n", dp[m]);
}
单调队列优化法
…………
4.分组背包
思维导图:
其实本质上还是可以用01背包的思想。
代码如下:
#include <iostream>
using namespace std;
const int N = 110;
int v[N][N], w[N][N];
int dp[N];
int s[N];
int n, m;
int main() {
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; i++) {
scanf("%d", &s[i]);
for(int j = 0; j < s[i]; j++) {
scanf("%d%d", &v[i][j], &w[i][j]);
}
}
for(int i = 1; i <= n; i++) {
for(int j = m; j >= 0; j--) {
for(int k = 0; k< s[i]; k++) {
if(v[i][k] <= j) dp[j] = max(dp[j], dp[j - v[i][k]] + w[i][k]);
}
}
}
printf("%d\n", dp[m]);
}