背包问题理论基础(滚动数组)

二维dp数组dp[i][j]表示从【0-i】中任取物品装在容量为j的背包,背包中的最大价值为dp[i][j]
二维dp数组的公式:
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i])
其实可以发现如果把dp[i - 1]那一层拷贝到dp[i]上,表达式完全可以是:
dp[i][j] = max(dp[i][j], dp[i][j - weight[i]] + value[i]);
可以发现此时i维度完全可以省略,因此一维dp数组的公式为dp[j]=max(dp[j], dp[j - weight[i]] + value[i])
因此dp[j]实际上表示的还是从【0-i】中任取物品装在容量为j的背包,背包中的最大价值为dp[i][j],但是下标i被省略了,因此当计算dp[j],也就是要计算dp[i][j]时,此时dp[j]中存储的是dp[i-1][j]的值

动规五部曲分析如下:
1.确定dp数组的定义
在一维dp数组中,dp[j]表示:容量为j的背包,所背的物品价值可以最大为dp[j],而实际上dp[j]还存在着一个被隐藏的维度i,若此时求的dp[j]是dp[i][j],即表示此时已经遍历完物品[0-(i-1)],此时正在遍历物品i

2.一维dp数组的递推公式
dp[j]为 容量为j的背包所背的最大价值,那么如何推导dp[j]呢?
上面分析一维数组的定义已经知道了若想求dp[j],假设遍历到了物品i,则实际上此时dp[j]中存储着的是dp[i-1][j],即已经知道了当背包容量为j时,在物品0-(i-1)中任取,背包中的最大价值为dp[j],用二维dp数组来理解的话实际储存的值就是dp[i-1][j]
因此当遍历到物品i时,dp[j]的取值有两种可能性:一种可能性是向背包内装入物品i,此时背包内的最大价值就是物品i的价值加上背包容量j减去了物品i的体积weight[i]之后的剩下容量所能装下的最大价值,即dp[j - weight[i]]+ value[i],dp[j - weight[i]]表示容量为j - weight[i]的背包所背的最大价值。dp[j - weight[i]] + value[i] 表示 容量为 j - 物品i重量 的背包 加上 物品i的价值。,另一个可能性就是不向背包内放入物品i,此时背包所能装的最大价值就是在标号为[0-(i-1)]的物品中任取所能取得最大价值,也就是dp[j]。
总结来说就是,dp[j]的更新过程中有两个选择,一个是取dp[j] 相当于 二维dp数组中的dp[i-1][j],即不放物品i,一个是取dp[j - weight[i]] + value[i],即放物品i,选取两个选择中取值最大的,毕竟是求最大价值。

dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);

3.一维dp数组如何初始化
关于初始化,一定要和dp数组的定义吻合,否则到递推公式的时候就会越来越乱。

dp[j]表示:容量为j的背包,所背的物品价值可以最大为dp[j],那么dp[0]就应该是0,因为背包容量为0所背的物品的最大价值就是0。而结合隐藏的维度i进行考虑,初始化时并没有开始遍历物品,因此初始化值也应该是0.

那么dp数组除了下标0的位置,初始为0,其他下标应该初始化多少呢?

看一下递归公式:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);

dp数组在推导的时候一定是取价值最大的数,如果题目给的价值都是正整数那么非0下标都初始化为0就可以了。而结合隐藏的维度i进行考虑,初始化时并没有开始遍历物品,因此初始化值也应该小于可能取到的价值的最小值。
这样才能让dp数组在递归公式的过程中取的最大的价值,而不是被初始值覆盖了。
那么我假设物品价值都是大于0的,所以dp数组初始化的时候,都初始为0就可以了。

4.一维dp数组遍历顺序
代码如下:

for(int i = 0; i < weight.size(); i++) { // 遍历物品
    for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
        dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);

    }
}

可以看到一维dp遍历的时候,背包容量是从大到小,这样是为了保证物品i只被放入一次。

题目类型及其递归公式:
1.纯0-1背包:是求 给定背包容量 装满背包 的最大价值是多少。

for(int i = 0; i < weight.size(); i++) { // 遍历物品
    for(int j = bagWeight; j >= weight[i]; j--) { // 倒序遍历背包容量是为了保障每个物品最多只被取一次
        dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
    }
}
  1. 分割等和子集 (opens new window)是求 给定背包容量,能不能装满这个背包。
for(int i = 0; i < weight.size(); i++) { // 遍历物品
    for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
        dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
    }
}

若dp[bagWeight]==bagWeight,则说明能够装满背包

  1. 最后一块石头的重量 II (opens new window)是求 给定背包容量,尽可能装,最多能装多少
for (int i = 0; i < stones.size(); i++) { // 遍历物品
            for (int j = target; j >= stones[i]; j--) { // 遍历背包
                dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]);
            }
        }

和0-1背包稍有变化的在于这道题的重量和价值都是stone[i]
前面这三道题所用的公式其实都是一致的,都是:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]),区别在于根据题目要求识别出公式中的weight、value数组对应题目中的哪个数组。

  1. 目标和 (opens new window)是求 给定背包容量,装满背包有多少种方法,注意一下求装满背包有多少种方法一般都是用公式:dp[j] += dp[j - nums[i]]
for (int i = 0; i < nums.size(); i++) {
            for (int j = bagSize; j >= nums[i]; j--) {
                dp[j] += dp[j - nums[i]];
            }
        }

6.完全背包理论(和0-1背包的唯一区别就是物品可以无限的被添加,代码上的体现就是遍历背包从前向后遍历

// 先遍历物品,再遍历背包
for(int i = 0; i < weight.size(); i++) { // 遍历物品
    for(int j = weight[i]; j <= bagWeight ; j++) { // 遍历背包容量
        dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);

    }
}

7.完全背包中的给定背包容量,装满背包有多少种方法(组合问题),详细举例如下:
给定物品集合:nums = [1,2,3], 背包容量:target = 4

这里使用的是一维数组dp[j],当前物品遍历到i,则此刻dp[j]存储的实际是dp[i-1][j],即在物品[0-(i-1)]中任取,能够装满背包共有多少种方法:
dp数组初始化为,
0 1 2 3 4
1 0 0 0 0

当i=0,
j=1 dp[1]=dp[1]+dp[1-1]=0+1=1 [1](dp[1]表示在物品[0-(0-1)]中任取。这里的意思就是没有取物品的情况下,装满容量为1的背包的方法总数,当然是为0啦,也可以理解为统计背包没有装入物品0时,装满容量为1的背包的方法总数,而dp[1-1]表示背包装入物品0时,此时装满容量为1的背包的方法总数)

j=2 dp[2]=dp[2]+dp[2-1]=0+1=1 [1,1]
j=3 dp[3]=dp[3]+dp[3-1]=0+1=1 [1,1,1]
j=4 dp[4]=dp[4]+dp[4-1]=0+1=1 [1,1,1,1]
dp [1 1 1 1 1]

i=1
j=2 dp[2]=dp[2]+dp[2-2]=1+1=2 [1,1],[2]
j=3 dp[3]=dp[3]+dp[3-2]=1+1=2 [1,1,1],[1,2]
j=4 dp[4]=dp[4]+dp[4-2]=1+2=3 [1,1,1,1],[1,1,2],[2,2]
以上面dp[4]的计算会更加容易理解,等号后面的dp[4]表示背包没有装入物品1时,装满容量为4的背包的方法总数,dp[4-2]表示背包装入物品1时,装满容量为4的背包的方法总数,实际就是物品1的重量为2,若选择装入物品1,则要查看装满背包容量为4-weight[1]=4-2=2的方法总数有多少种,可以知道有两种组合,也就是上面j=1时已经求出的[1,1],[2],又因为此时加入物品1,因此加入物品1时的组合有[1,1,2],[2,2]
dp [1 1 2 2 3]

i=2
j=3 dp[3]=dp[3]+dp[3-3]=2+1=3 [1,1,1],[1,2],[3]
j=4 dp[4]=dp[4]+dp[4-3]=3+1=4 [1,1,1,1],[1,1,2],[2,2],[4]

上面的举例可以看出完全背包和0-1背包的区别也就在于对背包遍历顺序的不同,完全背包是背包遍历顺序从前向后,这样就实现了元素可以重复选取的要求,而0-1背包则是背包遍历顺序从后向前,这样就实现了每个元素只能取一次的要求。

8.完全背包中的给定背包容量,装满背包有多少种方法(排列问题,即【1,2】和【2,1】)算两种情况)
方法:
如果求组合数就是外层for循环遍历物品,内层for遍历背包。
即上面序号7组合问题的求解一样。

如果求排列数就是外层for遍历背包,内层for循环遍历物品。

同样以nums = [1,2,3], target = 4举例。
初始化dp[j]=[1 0 0 0 0],dp[0]=1的意义是当装满容量为0的背包有1种方法,即空背包情况[ ],物品序号为i,背包序号为j。
遍历物品时背包体积大于物品体积才有意义,即只考虑j>=weight[i]的情况,且i从小到大遍历(保证每个物品可被取多次)
j=1
i=0 dp[1]=dp[1]+dp[1-1]=0+1=1 [1]
第一个dp[1]表示若不放入物品0,装满容量为1的背包有几种方法,此时dp[1]当然等于0,而dp[1-1]表示若装入物品0,装满容量为1的背包有多少种方法,实际就是背包容量为1-1=0时的情况即背包为空加上物品0,也就是[1]这种情况。
i=1 nums[i]=2>j break
dp=[1 1 0 0 0]

j=2
i=0 dp[2]=dp[2]+dp[2-1]=0+1=1 [1,1]
i=1 dp[2]=dp[2]+dp[2-2]=1+1=2 [1,1],[2]
dp=[1 1 2 0 0]

j=3
i=0 dp[3]=dp[3]+dp[3-1]=0+2=2 [1,1,1],[2,1]
每次遍历到一个新的背包空间。如上面的j=3,i=0,第一个dp[3]表示若不放入物品0,装满容量为3的背包有几种方法,因为这是刚刚遍历到容量为3的背包,因此dp[3]此时是等于0的,而dp[3-1]表示若放入物品0,装满容量为3的背包有几种方法,实际上就是dp[3-1]=dp[2]时的情况集合加入元素0,dp[2]中的情况有两种[1,1,],[2],在这两种情况下再分别加入元素0,也就是[1,1,1]和[2,1]这两种情况。

i=1 dp[3]=dp[3]+dp[3-2]=2+1=3 [1,1,1],[2,1],[1,2]
i=2 dp[3]=dp[3]+dp[3-3]=3+1=4 [1,1,1],[2,1],[1,2],[3]
dp=[1 1 2 4 0]

j=4
i=0 dp[4]=dp[4]+dp[4-1]=0+4=4 [1,1,1,1],[2,1,1],[1,2,1],[3,1]
i=1 dp[4]=dp[4]+dp[4-2]=4+2=6 [1,1,1,1],[2,1,1],[1,2,1],[3,1],[1,1,2],[2,2]
i=2 dp[4]=dp[4]+dp[4-3]=6+1=7 [1,1,1,1],[2,1,1],[1,2,1],[3,1],[1,1,2],[2,2],[1,3]
dp=[1 1 2 4 7]

总结:
确定dp[j]的实际意义

元素只能取一次(0-1背包)的代码体现就是背包容量遍历顺序为从大到小

元素能无限次的选取(组合问题)的代码体现是背包容量遍历顺序为从小到大

排列问题的代码体现是外层遍历背包,内层遍历物品,

组合问题的代码体现是外层遍历物品,内层遍历背包

递推公式:

容量为j的背包,所背的物品价值可以最大为dp[j]:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);

求组合数:dp[j] += dp[j - nums[i]];

求满足条件组合中的最小长度:dp[j] = min(dp[j - coins[i]] + 1, dp[j]);

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值