2021年1月10日 时间都去哪了?
今日计划:力扣题目刷到150。javaweb进行下去,或者学习计算机网络。
今日工作:力扣题目刷到147。主要是背包问题还挺难搞???
1.背包问题的学习+整理。
2.416. 分割等和子集
3.解决了Typora笔记上传到CSDN的图片失效问题。
4.逛县城。试做小火锅,经验++。傍晚跑步
今日总结:
在家的效率是打了大大的折扣。
年前补基础,年后找实习。✔
今日语录:
女作家多丽丝·莱辛在《幸存者回忆录》中,写过这样一段话:
“我们浪费自己的健康,去赢得个人的财富,然后又浪费自己的财富,去重建自己的健康。
“我们焦虑地憧憬着未来,忘记了眼前的生活,活得既不是为了现在,也不是为了未来。
“我们活得似乎永远不会死,我们死得好像从来没活过。”
明天计划:整理背包问题的笔记。力扣题目刷到150+。
明天是新的一周了。下周刷题20道。力扣到达170。开始学习项目吧。
可以研读一下:大雪菜的背包九讲+也可以加入大雪菜的寒假刷题班5元
转到2021.1.9-2021.1.31的learning record 首页
相关问题
「力扣」上的 0-1 背包问题:
「力扣」第 416 题:分割等和子集(中等);
「力扣」第 474 题:一和零(中等);
「力扣」第 494 题:目标和(中等);
「力扣」第 879 题:盈利计划(困难);
「力扣」上的 完全背包问题:
「力扣」第 322 题:零钱兑换(中等);
「力扣」第 518 题:零钱兑换 II(中等);
「力扣」第 1449 题:数位成本和为目标值的最大数字(困难)。
这里要注意鉴别:「力扣」第 377 题,不是「完全背包」问题。
作者:liweiwei1419
链接:https://leetcode-cn.com/problems/partition-equal-subset-sum/solution/0-1-bei-bao-wen-ti-xiang-jie-zhen-dui-ben-ti-de-yo/
贪心算法:优先放入平均价值最高的物品。
本题通过实际的案例,发现在这里贪心算法不可以得到全局最优解。
通过背包问题,引入动态规划的解决办法。
背包容量c固定,具有重量和价值特征的n种物品,挑出0-n个物品,装进背包,使得背包装入的物品总价值最高。
上图表示了0-1背包问题的状态转移方程。
F(n,c):将n个物品装进容量为c的背包,使得价值最大。
F(i,c)=max( F(i-1,c) , v(i) + F( i-1,c- w(i) ) )
F(i-1,c):第i个物品不放进背包中。
F( i-1,c- w(i) ) :第i个物品放进背包中,相应的,当前的背包容量=背包容量 - 物品i的重量w(i)。
public class p9_ex5_1_1 {
public int knapsack01(int[] w, int[] v, int C) {
int n = w.length;
//用[0,1,... ...,index]的物品来填充体积为c的背包的最大价值
// n-1 是说 物品 0到n-1
return bestValue(w, v, n - 1, C);
}
private int bestValue(int[] w, int[] v, int index, int c) {
// 边界条件
// 在调用过程中,index是逐渐减少的,容量c也是逐渐减少的。
if (index < 0 || c < 0) {
return 0;
}
//第index个物品不放入背包里。
int res = bestValue(w, v, index - 1, c);
// 判断剩余容量与w[index] 判断第 index 个物品的体积是否 小于等于背包的剩余容量。
// 否则直接返回上面得到的res。
if (w[index] <= c) {
//第index个物品放入背包里。
res = Math.max(res, v[index] + bestValue(w, v, index - 1, c - w[index]));
}
return res;
}
}
import java.util.Arrays;
// 记忆化搜索
public class p9_ex5_1_1_1 {
int[][] memo;
public int knapsack01(int[] w, int[] v, int C) {
int n = w.length;
// 这里的初始化 容量是 从0到c,是c+1个数。
memo = new int[n][C + 1];
for (int i = 0; i < n; i++) {
Arrays.fill(memo[i], -1);
}
//用[0,1,... ...,index]的物品来填充体积为c的背包的最大价值
// n-1 是说 物品 0到n-1
return bestValue(w, v, n - 1, C);
}
private int bestValue(int[] w, int[] v, int index, int c) {
// 边界条件
// 在调用过程中,index是逐渐减少的,容量c也是逐渐减少的。
if (index < 0 || c < 0) {
return 0;
}
if (memo[index][c] != -1) {
return memo[index][c];
}
//第index个物品不放入背包里。
int res = bestValue(w, v, index - 1, c);
// 判断剩余容量与w[index] 判断第 index 个物品的体积是否 小于等于背包的剩余容量。
// 否则直接返回上面得到的res。
if (w[index] <= c) {
//第index个物品放入背包里。
res = Math.max(res, v[index] + bestValue(w, v, index - 1, c - w[index]));
}
memo[index][c] = res;
return res;
}
}
import java.util.Arrays;
public class p9_ex5_3 {
public static void main(String[] args) {
int[] w = new int[]{
1, 2, 3};
int[] v = new int[]{
6, 10, 12};
int c = 5;
System.out.println(knapsack01(w, v, c));
}
private static int knapsack01(int[] w, int[] v, int c) {
int n = w.length;
// 没有可供选择的物品
if (n == 0) {
return 0;
}
// c+1是为了遍历0到c这些重量。 c+1 个元素 遍历列元素。
int[][] dp = new int[n][c + 1];
// dp 第0行的元素是多少。
// 因为下面的下面的双重for循环会使用i-1,所有先去计算得到i=0的数据。
// dp[0][j] 是说考虑放入第0件物品。如果剩余容量≥w[0],则可以放入。
for (int j = 0; j <= c; j++) {
// 如果剩余容量j≥第0个物品的重量,那就可以将第0个物品放入背包,此时背包的价值为v[0]
// 三目运算符
dp[0][j] = j >= w[0] ? v[0] : 0;
}
for (int i = 1; i < n; i++) {
for (int j = 0; j <= c; j++) {
// dp[i][j]是指考虑0到i这些物品,且容积为j的背包,所得到的最大值是多少。
// 1.不考虑第i件物品。
dp[i][j] = dp[i - 1][j];
// 剩余的容量j仍然可以将重量为w[i]的物品装进去。
if (j >= w[i]) {
dp[i][j] = Math.max(dp[i][j], v[i] + dp[i - 1][j - w[i]]);
}
}
}
// dp初始化大小为[n][c+1]
return dp[n - 1][c];
}
}
F(i,c)=max( F(i-1,c) , v(i) + F( i-1,c- w(i) ) )
F(i-1,c):第i个物品不放进背包中。
F( i-1,c- w(i) ) :第i个物品放进背包中,相应的,背包容量-物品i的重量w(i)。
第i行元素,只是依赖于第i-1行元素,因此理论上只保留两行元素即可。
代码如下:
public class p9_ex5_3_2_1 {
public static void main(String[] args) {
int[] w = new int[]{
1, 2, 3};
int[] v = new int[]{
6, 10, 12};
int c = 5;
System.out.println(knapsack01(w, v, c));
}
private static int knapsack01(int[] w, int[] v, int c) {
int n = w.length;
if (n == 0) {
return 0;
}
// 第i行元素,只是依赖于第i-1行元素,因此理论上只保留两行元素即可。
int[][] dp = new int[2][c + 1];
for (int j = 0; j <= c; j++) {
dp[0][j] = j >= w[0] ? v[0] : 0;
}
// i j 还是正常的去遍历。但是dp的空间是限制在了两行元素。
// dp[行的索引i或者i-1 % 2]
for (int i = 1; i < n; i++) {
for (int j = 0; j <= c; j++) {
dp[i % 2][j] = dp[(i - 1) % 2][j];
if (j >= w[i]) {
dp[i % 2][j] = Math.max(dp[i % 2][j], v[i] + dp[(i - 1) % 2][j - w[i]]);
}
}
}
return dp[(n - 1) % 2][c];
}
}
[视频9-6 15min30s讲到了 0-1背包问题的变种]
但是并没有详细去引申。自己可以去研读一下:大雪菜的背包九讲+也可以加入大雪菜的寒假刷题班5元。但是实际面试应该够了,不会考的很深。
完全背包问题:每个物品可以无限次的使用。这里由于背包的容量有限制,所以实际上每个物品的个数也会被限制,所以可以将完全背包问题转化为0-1背包问题。
多重背包问题:每个物品不止1个,而是有num[i]个。感觉就是上面问题的拓展延伸。
多维费用的背包问题:考虑体积和重量两个维度
对上面图片的解释:
题目要求:给定一个只包含正整数的非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。那这样两个子集的元素和相等,那么所有元素遍历求和,除以2,得到目标值。目标值需要是整数。sum%2!=0
F(n,c)这里的c=sum/2。F(n,c)是将n个物品填满容量为c的背包。
F(i,c)=F(i-1,c)||F(i-1,c-w(i))。||的解释:因为要求结果是布尔值,那么只要这两个条件中一条满足,即可返回true。
F(i-1,c)是说第i个物品不装进背包。
F(i-1,c-w(i))是说将第i个物品装进背包。
伪代码:
1.遍历数组元素求和,sum%2!=0
2.边界条件:sum=0.sum<0.index<0.
3.业务代码
4.实际上,你考虑:sum/2在一个业务代码中满足了,那么剩余的元素和肯定也是sum/2。
递归会超时。
public class p9_ex7_416 {
//用[0,......,index]的数组填充容量为sum的背包
public boolean canPartition(int[] nums) {
int n = nums.length;
int sum = 0;
// 首先,对输入的数组进行遍历求和。如果sum是偶数,则可以继续,否则直接返回false。
for (int i = 0; i < n; i++) {
sum = sum + nums[i];
}
// 如果sum不是偶数。那么就不能被平分。
if (sum % 2 != 0) {
return false;
}
// n-1 是对0到n-1的物品进行处理。
// tryPartition解决了 0到n-1的物品 能否填充 sum/2 的背包。
return tryPartition(nums, n - 1, sum / 2);
}
// 这里是使用nums[0...index]的物品,看是否能够完全填充一个容量为sum的背包
private boolean tryPartition(int[] num, int index, int sum) {
// 边界条件
if (sum == 0) {
return true;
}
// 边界条件
if (sum < 0 || index < 0) {
return false;
}
// 业务代码 F(i,c)=F(i-1,c)||F(i-1,c-w(i))
//F(i-1,c)是说第i个物品不装进背包。
//F(i-1,c-w(i))是说将第i个物品装进背包。
return tryPartition(num, index - 1, sum) || tryPartition(num, index - 1, sum - num[index]);
}
}
import java.util.Arrays;
public class p9_ex7_416 {
//memo[index][sum]用[0,......,index]的数组填充容量为sum的背包,是否可以填满
//-1表示未填充,0表示填充不满,1表示可以填充
int[][] memo;
//用[0,......,index]的数组填充容量为sum的背包
public boolean canPartition(int[] nums) {
int n = nums.length;
int sum = 0;
// 首先,对输入的数组进行遍历求和。如果sum是偶数,则可以继续,否则直接返回false。
for (int i = 0; i < n; i++) {
sum = sum + nums[i];
}
// 如果sum不是偶数。那么就不能被平分。
if (sum % 2 != 0) {
return false;
}
memo = new int[n][sum / 2 + 1];
for (int i = 0; i < n; i++) {
Arrays.fill(memo[i], -1);
}
// n-1 是对0到n-1的物品进行处理。
// tryPartition解决了 0到n-1的物品 能否填充 sum/2 的背包。
return tryPartition(nums, n - 1, sum / 2);
}
// 这里是使用nums[0...index]的物品,看是否能够完全填充一个容量为sum的背包
private boolean tryPartition(int[] num, int index, int sum) {
// 边界条件
if (sum == 0) {
return true;
}
// 边界条件
if (sum < 0 || index < 0) {
return false;
}
if(memo[index][sum] != -1)
return memo[index][sum]==1?true:false;
memo[index][sum]=(tryPartition(nums,index-1,sum)||tryPartition(nums,index-1,sum-nums[index]))==true?1:0;
return tryPartition(nums,index-1,sum)||tryPartition(nums,index-1,sum-nums[index]);
}
}
性能还是差一点。
这个是看的力扣的题解:+感觉比视频要好理解的多。
其实我还是感觉记忆化搜索比动态规划容易理解。
public class p9_ex7_416_1_1_1 {
public static void main(String[] args) {
int[] nums = new int[]{
1, 5, 11, 5};
System.out.println(canPartition(nums));
}
public static boolean canPartition(int[] nums) {
int n = nums.length;
int sum = 0;
for (int i = 0; i < n; i++) {
sum += nums[i];
}
if (sum % 2 != 0) {
return false;
}
int c = sum / 2;
// 这里的数组初始化后,默认值是false。
boolean[][] dp = new boolean[n][c + 1];
// 因为下面的句子会用到dp[i - 1][j]。所以先填表格第 0 行,第 1 个数只能让容积为它自己的背包恰好装满
if (nums[0] <= c) {
dp[0][nums[0]] = true;
}
for (int i = 1; i < n; i++) {
for (int j = 0; j <= c; j++) {
//不将第i件物品装进背包。
dp[i][j] = dp[i - 1][j];
// 下面的if可以删掉,不影响结果。
if (nums[i] == j) {
dp[i][j] = true;
continue;
}
// 剩余容量可以装入第i件物品
if (nums[i] < j) {
dp[i][j] = dp[i - 1][j] || dp[i - 1][j - nums[i]];
}
}
}
return dp[n - 1][c];
}
}