什么是01背包问题?
01背包问题的题目描述一般是:给你一个正数m,代表背包的容量。此时有n个物品,每个物品都有自己的体积costs[i]和价值values[i],每个物品只能选择一次。问在不超过背包容量m的情况下,让你尽可能的往背包里放物品,你能让背包装载的最大价值是多少?
如何明确01背包的dp数组定义及状态转移方程?
(1)解决01背包问题一般用二维dp数组,那么dp[i][j]的含义是:对于前i个物品,背包容量为j的情况下,背包所能装载的最大价值是dp[i][j]。
(2)对于第i个物品,我们只有两种选择:1.不放入背包,dp[i][j] = dp[i - 1][j],不放入背包不就意味着你的最优解与前i - 1个物品相关嘛; 2.放入背包,dp[i][j] = dp[i - 1][j - costs[i]] + values[i],放入背包前,需要确认一件事:j - costs[i] > 0,背包容量没有到达上限。因为我们选择将第i个物品放入背包,所以我们需要加上它相应的价值values[i]。
(3)最后在放入背包与不放入背包之间选择一个较大值。
//外层循环代表物品前i个物品,前0个物品无论怎么放,得到的最大价值都是0
//所以循环从1开始。
for(int i = 1;i < n + 1;i ++){
for(int j = 0;j < m + 1;j ++{
//选择不放入背包
dp[i][j] = dp[i - 1][j];
//背包容量允许第i个物品放入背包时
if(j - costs[i] > 0){
//在不放入背包和放入背包中选择一个较大值
dp[i][j] = Math.max(dp[i - 1][j],dp[i - 1][j - costs[i]] + values[i]);
}
}
}
题目1:416. 分割等和子集 - 力扣(LeetCode)
题目描述:
问题分析:
(1)根据题目描述我们可以知道,题目是想让我们找出一个子序列,这个子序列的和等于数组所有元素之和的一半。
(2)那我们是不是可以把问题转化成:给你一个容量为sum / 2的背包,让你用数组中的元素去填满这个背包,问是否能恰好填满这个背包。
Code:
class Solution {
public boolean canPartition(int[] nums) {
int n = nums.length;
int sum = 0;
for(int x : nums)
sum += x;
//数组总和为奇数时,无论怎么分都无法分出两个值相等的子集
if(sum % 2 != 0)
return false;
sum /= 2;
//dp[i][j]含义:对于可选择的前i个物品,当背包容量为j时,是否能用这前i个物品恰好填满背包
boolean [][] dp = new boolean[n + 1][sum + 1];
//背包容量为0时,前i个物品总能填满背包
for(int i = 0;i < n + 1;i++)
dp[i][0] = true;
for(int i = 1;i < n + 1;i ++){
for(int j = 1;j < sum + 1;j ++){
//由于背包容量限制,不允许第i个物品放入
if(j - nums[i - 1] < 0){
dp[i][j] = dp[i - 1][j];
}else{
//背包可以放下第i个物品,只要有一个满足要求即可
dp[i][j] = dp[i - 1][j] || dp[i - 1][j - nums[i - 1]];
}
}
}
//dp[n][sum]:对于数组中的n个元素,是否有一种组合能恰好填满背包容量sum
return dp[n][sum];
}
}
题目2:1049. 最后一块石头的重量 II - 力扣(LeetCode)
题目描述:
问题分析:
(1)首先我们对石头数组做出假设,假设它为:[1,7,8],它的最优解是什么?应该是8与7相撞变成1,再跟1相撞变成0,这就是最优解;那石头数组为:[5,6,7,8],它的最优解又是什么?应该是8与6相撞变成2,7与5相撞变成2,再让两个2相撞变成0,这就是最优解。
(2)根据上述例子与题目1我们可以想到什么?将石头数组进行分组![1,7,8]中,1和7属于集合A,8属于集合B;[5,6,7,8]中,6和7属于集合A,5和8属于集合B。两个集合的差值就是剩下的石头最小的可能重量。
(3)所以我们要做的就是让数组尽量分成数值接近的两个集合,这两个集合的差值就是我们需要的答案。
Code:
class Solution {
public int lastStoneWeightII(int[] stones) {
int n = stones.length;
int sum = 0;
for(int stone : stones)
sum += stone;
//让集合尽量等分成数组总和的一半
//这样能让两个集合的差值最小
sum /= 2;
//dp[i][j]含义:对于前n个石头,在不超过sum的情况下,所能装载的最大值为dp[i][j]
int [][] dp = new int[n + 1][sum + 1];
for(int i = 1;i <= n;i ++){
for(int j = 0;j <= sum;j ++){
//第i个物品不放入背包的情况
dp[i][j] = dp[i - 1][j];
//如果背包的剩余容量允许第i个物品放入背包
if(j - stones[i - 1] >= 0){
//则在将第i个物品放入背包和不放入背包中取一个较大值
dp[i][j] = Math.max(dp[i][j],dp[i - 1][j - stones[i - 1]] + stones[i - 1]);
}
}
}
int sum2 = 0;
for(int stone : stones)
sum2 += stone;
return sum2 - dp[n][sum] - dp[n][sum];
}
}
补充说明:
返回结果为什么是数组总和 - dp[n][sum] - dp[n][sum] ?
(1)首先dp数组的目的是:在原数组中求一个子序列,这个子序列的和在<=sum的情况下的最大值。以数组[5,5,6,7]为例,数组总和是23,sum向下取整等于11,数组中5与6的和恰好等于11,所以dp[n][sum] = 11.
(2)已经求出dp[n][sum] = 11了,其中一个集合的值已经确定了,那么另外一个集合的值不就是数组总和 - dp[n][sum]吗?同时,我们之前通过分析知道答案与两个集合的差值有关,那结果不就是:数组总和 - dp[n][sum] - dp[n][sum]嘛。