理论基础:二维数组
-
《算法图解》第九章背包的讲解更好的帮助理解
动规五部曲:
确定dp数组以及下标的含义:使用二维数组,即dp[i][j] 表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少。
- 确定递推公式:关键是在不放与放物品i的两个状态中取价值更大的那一种。 dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i])
- dp数组如何初始化:dp[i][0] = 0, dp[0][j]; 对于dp[0][j]分两种情况,一是 j < weight[0]的时候,dp[0][j] = 0,二是当j >= weight[0]时,dp[0][j] 应该是value[0]。那么其他的部分则可以初始化为任意值因为都会被更新。
- 确定遍历顺序:先遍历 物品还是先遍历背包重量呢?其实都可以!! 但是先遍历物品后遍历背包更好理解。(既然二维数组已经定义了,那么就按照默认的先行后列来遍历)
- 举例推导dp数组
Q:对于递推公式,难道放还会比不放得到的价值更低吗?
A:状态1: 选择放实际上对前面的操作是有要求的,需要有足够的重量空间来放入,甚至要挤掉某个物品;状态2: 选择不放此时就需要前面的操作实现价值最大化。这样就需要对比两种状态下哪个得到的价值更大。
-
测试代码参考网站:代码随想录
理论基础:滚动数组(一维数组)
Q:如何实现的降维?
A:其实就是只使用一行(将一个二维矩阵压缩成一行的数组),每次都更新这一行的数据(前提是当前行是由上一行推导得来),因此也就没有i了,此时i=0了。
(说白了,最终要的结果在右下角,那么其余位置的值都是为了推导出这个结果,所以不需要保存下来,使用一次直接舍弃就好,因此可以降维)
动规五部曲:
- 确定dp数组的定义:dp[j]表示容量为j的背包,所背的物品价值可以最大为dp[j]
- 一维dp数组的递推公式:dp[j] = max(dp[j], dp[j - weight[i]] + value[i])
- 一维dp数组如何初始化:dp[0] = 0; 其余的要初始化成一个非负数的最小值,这样才能实现求max的更新,那么就都初始化为0。
- 一维dp数组遍历顺序:从后先前(背包容量由大到小)
- 举例推导dp数组
Q:为什么遍历顺序是后->前?
A:因为在二维数组的情况时,当前值是由正上方和左上方的值推导得来,每一层的数据都是隔离的。那么降到一维数组时,就只有左方的值会影响当前值。如果还按照从前向后的顺序,每次更新后的值依然会对后续值产生影响。为了避免这个问题,所以需要从后向前遍历。
下面附上视频留言的帮助理解:
- “列表后面的值需要通过与前面的值比较确定,因此要先处理”
- 01背包每个背包都只有一个,这个包放与不放与上一个包的状态有关,而左边的数组是描述上一个包状态的量,右边的数是当前包状态的一个待定,代码里就是右边数组的确定需要左边的数,所以在计算出右边的数之前不能破坏左边的数;
测试代码参考网站:代码随想录
416. 分割等和子集(medium)
注意:
- 一个商品如果可以重复多次放入是完全背包问题,而只能放入一次则是01背包问题
本题经过分析得出主要意思是:
- 判断集合里能否出现总和为 sum / 2 的子集
只有确定了如下四点,才能把01背包问题套到本题上来。
- 背包的体积为sum / 2
- 背包要放入的商品(集合里的元素)重量为 元素的数值,价值也为元素的数值
- 背包如果正好装满,说明找到了总和为 sum / 2 的子集。
- 背包中每一个元素是不可重复放入。
动规五部曲:
- 确定dp数组以及下标的含义:dp[j]表示背包总容量是j,最大可以凑成j的子集总和为dp[j]
- 确定递推公式:dp[j] = max(dp[j], dp[j - nums[i]] + nums[i])
- dp数组如何初始化: 首先dp[0]一定是0; 如果题目给的价值都是正整数那么非0下标都初始化为0就可以了,如果题目给的价值有负数,那么非0下标就要初始化为负无穷。(因此为了统一,可以直接将非0下标都初始化为负无穷,自己测试。。。)
- 确定遍历顺序:如果使用一维dp数组,物品遍历的for循环放在外层,遍历背包的for循环放在内层,且内层for循环倒序遍历!(其实无论使用二维还是一维数组都先遍历物品就好)
- 举例推导dp数组:dp[j]的数值一定是小于等于j的(因为就这道题而言,物品i的重量是nums[i],其价值也是nums[i],因此j就代表了背包的容量,是上限)。如果dp[j] == j 说明,集合中的子集总和正好可以凑成总和j,理解这一点很重要。
class Solution(object):
def canPartition(self, nums):
"""
:type nums: List[int]
:rtype: bool
"""
if sum(nums) % 2 != 0:
return False
target = sum(nums) // 2
#step1: dp[j] 表示: 容量为j的背包,所背的物品价值可以最大为dp[j]
#step2: dp[j] = max(dp[j], dp[j-nums[i]]+nums[i])
#step3: 初始化:dp[0]=0, 由于nums数组内都是非负整数,那么其余的都初始化为0即可
dp = [0]*10001 #200*100 // 2 + 1
#step4: 遍历顺序:滚动数组要用倒序遍历,且先物品后背包
for i in range(len(nums)):
for j in range(target, nums[i]-1, -1):
dp[j] = max(dp[j], dp[j-nums[i]]+nums[i])
#step5: 打印检查
return target == dp[target]
相似题目:(之后再做。。。)
- 698.划分为k个相等的子集
- 473.火柴拼正方形