01背包理论基础
链接
理解
二维题解
例题: 背包最大重量为4。
物品为:
重量 | 价值 | |
---|---|---|
物品0 | 1 | 15 |
物品1 | 3 | 20 |
物品2 | 4 | 30 |
问背包能背的物品最大价值是多少? 01背包问题的难度很大我感觉,首先是dp数组的下表就不好确定,这里先说二维数组的dp[i][j]的含义为背包为j大小的最大价值,那i是什么意思呢? i用来表示装不装重量为weight[i]的物品,如图:
那么确定好dp数组的含义了,那么接下来需要确定初始化,由上面的那张图可以看出,j=0时价值应该都为0,对于dp[0][j]呢,只有j>=weight[0]的时候,dp[0][j]才等于value[i],如图:
那么初始化决定好了,之后就要递推公式了,那么dp[i][j]的价值最大,就要看放不放i这个物品了,j<weight[i],说明一定放不了所以dp[i][j]=dp[i-1][j],那么如果j>=weight[i],则需要判断放不放物品i了,不放的话价值就是dp[i-1][j],放的话就是dp[i-1][j-weight[i]]+value[i],所以比较他俩的大小就行了,代码如下:
for (let i = 1; i < weight.length; i++) { //物品
for (let j = 0; j <= size; 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])
}
}
}
同时这道题的遍历顺序也很重要,二维数组的遍历顺序其实是没有区别的,因为都是向上向左找,但是一维数组的就不一样了,下面是二维数组的代码:
function foo(weight: number[], value: number[], size: number) {
let dp: number[][] = new Array(weight.length).fill(0).map((item) => new Array(size + 1).fill(0)) //初始化
for (let j = weight[0]; j < dp[0].length; j++) {
dp[0][j] = value[0]
}
console.log(dp)
for (let i = 1; i < weight.length; i++) {
for (let j = 0; j <= size; 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])
}
}
}
console.log(dp)
console.log(dp[weight.length - 1][size])
}
foo([1, 3, 4], [15, 20, 30], 4)
一维题解
由于dp[i][j]的价值是由dp[i-1]推出来,所以可以转变为滚动数组,的还是按照递归五部曲,
- 首先是dp数组的含义很容易,dp[j]的含义为背包容量为j的最大价值
- 第二步是初始化,也很容易,全为0.
- 那么第三步就是重点了,因为要让背包容量为j的价值最大,所以递推公式为
dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i])
,等号右边的dp[j]是不放物品i时的价值。dp[j - weight[i]] + value[i]是放物品i的价值。 - 第四步,也是变化最大的,原因是对于背包的遍历是倒序的,原因是正序会让一个物品被装进多次,例如物品0的value[0]=15,weight[0]=1,所以dp[1]=15,dp[2]=dp[2-wight[0]]+value[0]=30,这就说明物品0被添加了两次 代码如下:
function foo(weight: number[], value: number[], size: number) {
let dp: number[] = new Array(size + 1).fill(0)
for (let i = 0; i < weight.length; i++) {//遍历物品
for (let j = size; j >= weight[i]; j--) {//遍历背包容量
dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i])//获取最大价值
}
}
console.log(dp)
console.log(dp[size])
}
foo([1, 3, 4], [15, 20, 30], 4)
注意不能先遍历背包容量,因为背包容量要倒序,需要使用物品重量
416. 分割等和子集
链接
文章链接
题目链接
第一想法
这道题按照递归五部曲来走
- dp数组的含义 我定义dp[i]为容量为i的背包和为dp[i],所以只需要判断sum/2===dp[sum/2]就可以了
- 初始化 很简单,因为我写的是一维数组,所以初始化全为0就可以了
- 递推公式为
arr[j] = Math.max(arr[j], arr[j - nums[i]] + nums[i])
,arr[j]为不算当前值的总和,arr[j - nums[i]] + nums[i]为算上当前数的总和 - 遍历顺序,对于一维数组,必须先是物品后是背包容量
这道题就是背包问题的变形,它的weight和value就是数值,都是一样的数值而已。代码如下:
//一维数组
function canPartition(nums: number[]): boolean {
let sum: number = 0
for (let i = 0; i < nums.length; i++) {//求和
sum += nums[i]
}
if (sum % 2 === 1) return false//和为奇数说明肯定平分不了
let arr: number[] = new Array(sum / 2 + 1).fill(0)//dp数组
for (let i = 0; i < nums.length; i++) {
for (let j = sum / 2; j >= nums[i]; j--) {
arr[j] = Math.max(arr[j], arr[j - nums[i]] + nums[i])//递归
}
}
return sum / 2 == arr[sum / 2]
}
//二维数组
function canPartition(nums: number[]): boolean {
let sum: number = 0
for (let i = 0; i < nums.length; i++) {
sum += nums[i]
}
if (sum % 2 === 1) return false
let arr: number[][] = new Array(nums.length).fill(0).map((item) => new Array(sum / 2 + 1).fill(0))//dp数组
for (let j = 0; j < sum / 2 + 1; j++) {//初始化
if (nums[0] <= j) arr[0][j] = nums[0]
}
for (let i = 1; i < nums.length; i++) {
for (let j = 0; j < sum / 2 + 1; j++) {
if (j < nums[i]) arr[i][j] = arr[i - 1][j]
else {
arr[i][j] = Math.max(arr[i - 1][j], arr[i - 1][j - nums[i]] + nums[i])
}
}
}
return sum / 2 == arr[nums.length - 1][sum / 2]
}
看完文章后的想法
文章的想法和我想的是一致的,所以这里就不再复制代码了。主要讲讲文章的思路:如果能看出这四点就知道这是个0-1背包问题:
- 背包容量为sum/2
- 物品的重量为nums[i],价值也是nums[i]
- 每一个元素不可以重复放入背包中
- 如果下标为sum/2的背包装的物品价值为sum/2说明是符合题意的
思考
这道题不算太难,原因是想到了和0-1背包问题的相似之处,所以九做出来了
今日总结
今天吃初步认识0-1背包,两种写法都有一定的难度,我个人倾向于用一位数组,因为我认为滚动背包容易理解,今天就一道力扣上的题,如果能把题想到0-1背包问题的话,搞清需要符合的条件以及weight和value就可以解决这道题了