一、二维数组
1、状态转移方程:
- 不放物品i:由dp[i - 1][j]推出,即背包容量为j,里面不放物品i的最大价值,此时dp[i][j]就是dp[i - 1][j]。(其实就是当物品i的重量大于背包j的重量时,物品i无法放进背包中,所以背包内的价值依然和前面相同。)
- 放物品i:由dp[i - 1][j - weight[i]]推出,dp[i - 1][j - weight[i]] 为背包容量为j - weight[i]的时候不放物品i的最大价值,那么dp[i - 1][j - weight[i]] + value[i] (物品i的价值),就是背包放物品i得到的最大价值
所以递归公式: dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
2、初始化
(1)从dp[i][j]的定义出发,如果背包容量j为0的话,即dp[i][0],无论是选取哪些物品,背包价值总和一定为0。
(2)状态转移方程 dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]); 可以看出i 是由 i-1 推导出来,那么i为0的时候就一定要初始化。
dp[0][j],即:i为0,存放编号0的物品的时候,各个容量的背包所能存放的最大价值。
那么很明显当 j < weight[0]的时候,dp[0][j] 应该是 0,因为背包容量比编号0的物品重量还小。
当j >= weight[0]时,dp[0][j] 应该是value[0],因为背包容量放足够放编号0物品。
java
for (int j = 0 ; j < weight[0]; j++) {
dp[0][j] = 0;
}
// 正序遍历
for (int j = weight[0]; j <= bagweight; j++) {
dp[0][j] = value[0];
}
(3)其他下标初始化:
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i])
dp[1][1] = max(dp[0][1], dp[0][1- weight[i]] + value[i]) 一定在其左方且上方,
故一定初始化过了,可以设置为任何值,为了方便,现默认初始化为0。
二、一维(滚动数组)
1、一维dp数组中,dp[j]表示:容量为j的背包,所背的物品价值可以最大为dp[j]
java
dp[j] = max(dp[j], dp[j - weight[i]] + value[i])
2、初始化
(1)dp[0]=0,因为背包容量为0所背的物品的最大价值就是0。
(2)假设物品价值都是大于0的,为了不被初始值覆盖,所以dp数组初始化的时候,都初始为0
3、遍历顺序
倒序遍历
解释1:列表后面的值需要通过与上一层遍历得到的前面的值比较确定
解释2:01背包每个背包都只有一个,这个包放于不放与上一个包的状态有关,而左边的数组是描述上一个包状态的量,右边的数是当前包状态的一个待定,代码里是右边数组的确定需要左边的数,所以在计算出右边的数之前不能破坏左边的数。
解释3:对于二维,我每个新的dp数字的结果,都来源于上面和左边;其实一维也本该是这样,但一维的时候,如果还是从左到右,那么我其实左边的数字已经更新过了(左边已经不是"原来的"左边了!这会造成某些重复计算 ),即使上面还是原来的上面,得到的数字也是不正确的。而如果我们在背包层(里层)从右到左地遍历,那么左边的元素永远不会因为我前一次的操作被覆盖上新的值,这样可以得到正确的值。
三、应用
1、给定一个只包含正整数的非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
注意: 每个数组中的元素不会超过 100 数组的大小不会超过 200
示例 1:
- 输入: [1, 5, 11, 5]
- 输出: true
- 解释: 数组可以分割成 [1, 5, 5] 和 [11]
分析:
物品是i,
重量是nums[i],价值也是nums[i],背包体积是sum/2。
类似于01背包
状态转移方程:
java
dp[j] = max(dp[j], dp[j - nums[i]] + nums[i])
java
// 开始 01背包
for(int i = 0; i < nums.size(); i++) {
for(int j = target; j >= nums[i]; j--) {
dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);
}
}
// 集合中的元素正好可以凑成总和target
if (dp[target] == target) return true;
2、有一堆石头,每块石头的重量都是正整数。
每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x 和 y,且 x <= y。那么粉碎的可能结果如下:
如果 x == y,那么两块石头都会被完全粉碎;
如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x。
最后,最多只会剩下一块石头。返回此石头最小的可能重量。如果没有石头剩下,就返回 0。
示例:
- 输入:[2,7,4,1,8,1]
- 输出:1
分析:
尽量让石头分成重量相同的两堆,相撞之后剩下的石头最小
那么分成两堆石头,一堆石头的总重量是dp[target],另一堆就是sum - dp[target]
在计算target的时候,target = sum / 2 因为是向下取整,所以sum - dp[target] 一定是大于等于dp[target]的
那么相撞之后剩下的最小石头重量就是 (sum - dp[target]) - dp[target]
java
for (int i = 0; i < stones.length; i++) {
//采用倒序
for (int j = target; j >= stones[i]; j--) {
dp[j] = Math.max(dp[j], dp[j - stones[i]] + stones[i]);
}
}
//结果
sum - 2 * dp[target];
引自:代码随想录