原作地址:https://blog.csdn.net/yoer77/article/details/70943462
0-1背包问题
有n个重量和价值分别为 wi,viwi,vi 的物品, 求所有挑选方案中价值总和的最大值。
样例:
n=4 (w,v)=(2,3),(1,2),(3,4),(2,2) W=5n=4 (w,v)=(2,3),(1,2),(3,4),(2,2) W=5
n个物品,每种物品只有两种选择,放进背包,或者不放进背包。n个物品对应的,最大的所有可能的总数为 2n2n 种不同的放法。
最朴素的,我们可以枚举所有可能的放法,找到最大值。
Rec(n,W)Rec(n,W)
总和两种可能,取较大值。可以给出递推式:
综上,得到递推式:
递归方法:
#include <iostream>
#define MAXN 10000
using namespace std;
int w[MAXN] = {0, 2, 1, 3, 2};
int v[MAXN] = {0, 3, 2, 4, 2};
int W = 5, n = 4;
int Rec(int i, int j) {
int res;
if (i == 0) {
// 终止条件, 无物品可选 Rec(0, j) = 0
// 0个物品可以选择,放入容量为j的背包, 得到的最大价值只能为0
res = 0;
}
else if (j < w[i]) {
// 背包剩余容量不足以放下第i个物品
res = Rec(i-1, j);
}
else {
// 抉择,第i个物品选或者不选,都试一下,取较大值
res = max(Rec(i-1, j), Rec(i-1, j-w[i]) + v[i]);
}
return res;
}
int main() {
cout << Rec(n, W) << endl;
return 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
大概画个递归过程:
Rec(4, 5)
/ \
Rec(3, 3) Rec(3, 5)
/ \ / \
Rec(2, 0) Rec(2, 3) Rec(2, 2) Rec(2, 5)
/ / \ / \ / \
Rec(1, 0) Rec(1, 2) Rec(1, 3) Rec(1, 1) Rec(1, 2) Rec(1, 4) Rec(1, 5)
/ / \ / \ / / \ / \ / \
(0,0) (0,0) (0,2) (0,1) (0,3) (0,1) (0,0) (0,2) (0,2) (0,4) (0,3) (0,5)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
从上图可以看出,我们枚举了所有可能的情况,即对于每个物品i,物品i选还是不选,我们都考虑了一遍。递归的层数是n+1层,最后一层是判定终止。
再来重申一下 Rec(i,j)Rec(i,j) 。
记忆化搜索
我们可以利用递归求解过程中的重复计算。如果Rec(i,j)Rec(i,j) 已经计算过,则记录下来,下次需要的时候直接拿来用即可。
#include <iostream>
#include <cstring>
#define MAXN 1000
using namespace std;
int w[MAXN] = {0, 2, 1, 3, 2};
int v[MAXN] = {0, 3, 2, 4, 2};
int dp[MAXN][MAXN]; //记录搜索过的结果
int W = 5, n = 4;
int Rec(int i, int j) {
//Rec(i, j)计算过,直接拿来用
if (dp[i][j] != -1) return dp[i][j];
int res;
if (i == 0) {
res = 0;
}
else if (j < w[i]) {
res = Rec(i-1, j);
}
else {
res = max(Rec(i-1, j), Rec(i-1, j-w[i]) + v[i]);
}
return dp[i][j] = res; //记录
}
int main() {
memset(dp, -1, sizeof(dp));
cout << Rec(n, W) << endl;
return 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
对于任意的i,j,Rec(i,j)i,j,Rec(i,j)
动态规划
上面的递归过程,是把大问题分解成小问题,最后由小问题的解合并成大问题的解。
动态规划的方法,就是先把小问题的解计算好,存在表里,等计算大问题的时候,需要用到小问题的解,就过来查一下表。
由递推式:
代码:
#include <iostream>
#include <cstring>
#define MAXN 1000
using namespace std;
int dp[MAXN][MAXN];
int w[MAXN] = {0, 2, 1, 3, 2};
int v[MAXN] = {0, 3, 2, 4, 2};
int W = 5, n = 4;
int solve(int n, int W) {
memset(dp, 0, sizeof(dp));
for (int i = 1; i <= n; i++) { //i从1开始,因为i=0的值已经确定为0
for (int j = 0; j <= W; j++) {
if (j < w[i]) {
dp[i][j] = dp[i-1][j];
}
else {
dp[i][j] = max(dp[i-1][j], dp[i-1][j-w[i]] + v[i]);
}
}
}
return dp[n][W];
}
int main() {
cout << solve(n, W) << endl;
return 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
dp 优化空间复杂度
输出一下上面的二维数组 dp[][]dp[][] :
0 0 0 0 0 0
0 0 3 3 3 3
0 2 3 5 5 5
0 2 3 5 6 7
0 2 3 5 6 7
我们是填dpdp 行的值以后都不会再用到了。
所以我们可以从这里入手优化空间复杂度。没有必要存下整个二维数组,我们只需要存2行,然后不断更新这2行就可以了。
实际上,dp[i][j]dp[i][j] 的值不会产生影响。
伪代码:
for i = 1 to n
for j = W to w[i]
dp[j] = max(dp[j], dp[j-w[i]] + v[i])
- 1
- 2
- 3
代码:
#include <iostream>
#include <cstring>
#define MAXN 10000
using namespace std;
int dp[MAXN];
int w[MAXN] = {0, 2, 1, 3, 2};
int v[MAXN] = {0, 3, 2, 4, 2};
int W = 5, n = 4;
int solve(int n, int W) {
memset(dp, 0, sizeof(dp));
for (int i = 1; i <= n; i++) { // i从1开始,递增
for (int j = W; j >= 0; j--) { // j按递减顺序填表
if (j < w[i]) {
dp[j] = dp[j];
}
else {
dp[j] = max(dp[j], dp[j-w[i]] + v[i]);
}
}
}
return dp[W];
}
int main() {
cout << solve(n, W) << endl;
return 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
初始化的细节问题
我们看到的求解最优解的背包问题中,事实和桑有两种不太相同的问法。
1. 要求”背包恰好装满“ 时的最优解
2. 不要求背包一定要被装满时的最优解
我们上面所讨论的就是第2种, 不要求背包一定要被装满时的最优解。
一种区别这两种问法的实现方法是在初始化的时候有所不不同。
如果是第一种问法,要求恰好装满背包,那么在初始化时除了 dp[0]dp[0] 全部设为0。
这是为什么呢?可以这样理解:初始化的dpdp 。当前的合法解,一定是从之前的合法状态推得的
如果背包并非必须被装满,那么任何容量的背包都有一个合法解 “什么也不装”,这个解的价值为0,所以初始化时状态的值也就全部为0了。
完全背包问题
有nn 的物品,求出挑选物品价值总和的最大值。在这里,每种物品可以挑选任意多件。
在0-1背包问题中,每种物品只有不选和选两种可能。在完全背包问题中,每个物品可以选0, 1, … ⌊W/wi⌋⌊W/wi⌋
代码:
#include <iostream>
#include <cstring>
#define MAXN 1000
using namespace std;
int dp[MAXN][MAXN];
int w[MAXN] = {0, 3, 4, 2};
int v[MAXN] = {0, 4, 5, 3};
int W = 7, n = 3;
int solve(int n, int W) {
memset(dp, 0, sizeof(dp));
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= W; j++) {
for (int k = 0; k <= j/w[i]; k++) {
dp[i][j] = max(dp[i][j], dp[i-1][j-k*w[i]] + k*v[i]);
}
}
}
return dp[n][W];
}
int main() {
cout << solve(n, W) << endl; // 10
return 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
最坏情况下复杂度为 O(nW2)O(nW2) 次,这是不必要的。动态规划就是要利用已经计算过的小规模的问题的解,来求解更大规模的问题的解。让我们来探寻一下,还有什么我们没有利用的重复计算。
再重申一下,dp[i][j]:=从前i种物品中挑