昨天,看了一天,让我真的狠狠受挫了。。昨天只看懂了二维数组,今天早上勉强?看懂了一维数组。。不能放弃!!加油!!
01背包-二维数组
(举例)
动态规划解题思路:
①确定dp数组以及下标含义
dp[i][j]:从[0,i]号物品随意取,在容量为j的背包下能产生的最大价值。
注意:i下标从0开始,物品编号为[0,n-1],j下标从0开始,容量编号为[0,size]。
定义为dp[n][size+1]
②确定递推公式
先遍历物品,再遍历容量(反过来也可以)
每次遍历时,首先进行判断当前j容量是否能装下i物品。
①当前j值代表容量能装下weight[i],即j>weight[i],此时可以选择装或者不装。
若装,则dp[i][j]=dp[i-1][j-weight[i]]+value[i] (j容量减去物品i容量值所能装的最大价值+i物品自身的价值);
若不装,则dp[i][j]=dp[i-1][j] (容量为j,物品从0-i1任取的最大容量)。
在这两者之前取大值即可。
②j<weight[i],即当前容量装不下物品i,则只能选择装[0,i-1]个物品
dp[i][j]=dp[i-1][j]
因此,递推公式为:
dp[i][j]=Math.max(dp[i-1][j],dp[i-1][j-weight[i]]+value[i])
③dp数组如何初始化
当j=0时,容量为0,什么也装不下dp[i][0]=0.
当i=0时,j>weight[i]才能装,因此给大于i容量的dp[0][j]赋值为value[0]即可,其余为初始值0.
for (int j = weight[0]; j <= bagweight; j++) {
dp[0][j] = value[0];
}
④确定遍历顺序
因为i=0已经被遍历过了,i从1开始遍历即可,i<物品(物品编号从0开始,最后一个物品下标为n-1)。
j=0时默认值0,j也从1开始遍历,j的编号为[0-size]。
先遍历背包或者先遍历物品都可以。
dp[i][j]=Math.max(dp[i-1][j],dp[i-1][j-weight[i]]+value[i]),dp[i][j]取决于上一行(i-1行)和左上行,所需要的数值不管是先哪个都是已知数,因此从前向后遍历即可,先遍历哪个都可以。
这里不是很理解,遍历顺序为啥先遍历哪个都可以。
⑤举例推导dp数组
例子中的dp数组如图:
public static void main(String[] args) {
int[] weight = {1, 3, 4};
int[] value = {15, 20, 30};
int bagweight = 4;
int wlen = weight.length, value0 = 0;
int[][] dp = new int[weight.length][bagweight + 1];
// 初始化
for (int j = weight[0]; j <= bagweight; j++) {
dp[0][j] = value[0];
}
// weight数组的大小 就是物品个数
for (int i = 1; i < weight.length; i++) { // 遍历物品
for (int j = 1; j <= bagweight; j++) { // 遍历背包容量
if (j < weight[i]) dp[i][j] = dp[i - 1][j];
else dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
}
}
for (int i = 0; i < weight.length; i++) {
for (int j = 0; j <= bagweight; j++) {
System.out.print(dp[i][j] + " ");
}
System.out.print("\n");
}
}
01背包-一维数组优化
从递推公式dp[i][j]=Math.max(dp[i-1][j],dp[i-1][j-weight[i]]+value[i])
可以发现,dp[i][j]只取决于上一层的值:
如果把dp[i - 1]那一层拷贝到dp[i]上,表达式完全可以是:dp[i][j] = max(dp[i][j], dp[i][j - weight[i]] + value[i]);
与其把dp[i - 1]这一层拷贝到dp[i]上,不如只用一个一维数组了,只用dp[j]覆盖掉原先的值。(一维数组,也可以理解是一个滚动数组)。
①确定dp数组以及下标含义
dp[j]表示:容量为j的背包,所背的物品价值可以最大为dp[j]。
②确定递推公式
去掉i,但是还是两层for循环遍历ij,不过不需要i,因为后面会覆盖掉前面的值,
dp[j] = max(dp[j], dp[j - weight[i]] + value[i])
③dp数组如何初始化
dp数组在推导的时候一定是取价值最大的数,如果题目给的价值都是正整数那么非0下标都初始化为0就可以了。
这样才能让dp数组在递归公式的过程中取的最大的价值,而不是被初始值覆盖了。
那么我假设物品价值都是大于0的,所以dp数组初始化的时候,都初始为0就可以了。
④确定遍历顺序
遍历容量j时,从最大容量向前遍历,遍历到weight[i]停止。
因为如果从前向后遍历,dp[i-1][j]前面的值已经被更新成dp[i][j]了,那么dp[j]就有可能把一个物品放进去两次。倒序遍历是为了保证物品i只被放入一次!
右下角的值依赖上一层左上角的值,因此需要保证左边的值仍然是上一层的,从右向左覆盖。
这里我画表格大概理解了,但是具体还不是很通透
先遍历物品,再遍历容量(反过来不可以)
个人理解为如果先遍历了容量,只知道dp上方的值,但是左上方的值还未确定,因此不能这样遍历。如果先遍历了容量,只知道dp上方的值,但是左上方的值还未确定,因此不能这样遍历。
⑤举例推导dp数组
public static void main(String[] args) {
int[] weight = {1, 3, 4};
int[] value = {15, 20, 30};
int bagweight = 4;
int wlen = weight.length, value0 = 0;
int[] dp = new int[bagweight + 1];
for (int i = 0; i < weight.length; i++) {
for (int j = bagweight; j >= weight[i]; j--) {
dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]);
}
}
for (int i = 0; i < dp.length; i++) {
System.out.println(dp[i]);
}
}
416. 分割等和子集
这道题就是我不看题解完全套不到背包问题上+看了题解也不是很理解为啥这样就可以了
转化为背包问题解题思路:
本题可以看成有一个sum/2容量的背包,有n个物品,每个物品的重量和价值都为nums[i],问是否有数能组合成sum的一半。若容量用完j==target时,最大价值dp[j]恰好等于容量target,即dp[target]==target。则符合题意。
public static boolean canPartition(int[] nums) {
int sum = 0;
for (int i : nums) {
sum += i;
}
if (sum % 2 == 1) {
return false;
}
int target = sum / 2;
int[] dp = new int[target + 1];
for (int i = 0; i < nums.length; i++) {
for (int j = target; j >= nums[i]; j--) {
dp[j] = Math.max(dp[j], dp[j - nums[i]] + nums[i]);
}
}
if (dp[target] == target) {
return true;
}
return false;
}