动态规划(一)

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]);
}
posted @ 2023-09-26 15:42  Brilliant11001  阅读(5)  评论(0编辑  收藏  举报