背包问题-ACM

11 篇文章 3 订阅
7 篇文章 0 订阅

原作地址: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)

总和两种可能,取较大值。可以给出递推式:

Rec(i,j)=max(Rec(i1,j),Rec(i1,jw[i])+v[i])Rec(i,j)=max(Rec(i−1,j),Rec(i−1,j−w[i])+v[i])

综上,得到递推式:

Rec(i,j)=⎧⎩⎨0Rec(i1,j)max(Rec(i1,j),Rec(i1,jw[i])+v[i])i=0j<w[i]otherRec(i,j)={0i=0Rec(i−1,j)j<w[i]max(Rec(i−1,j),Rec(i−1,j−w[i])+v[i])other

递归方法:

#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)

动态规划

上面的递归过程,是把大问题分解成小问题,最后由小问题的解合并成大问题的解。
动态规划的方法,就是先把小问题的解计算好,存在表里,等计算大问题的时候,需要用到小问题的解,就过来查一下表。
由递推式:

Rec(i,j)=⎧⎩⎨0Rec(i1,j)max(Rec(i1,j),Rec(i1,jw[i])+v[i])(i=0)(j<w[i])(other)Rec(i,j)={0(i=0)Rec(i−1,j)(j<w[i])max(Rec(i−1,j),Rec(i−1,j−w[i])+v[i])(other)

代码:

#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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值