LeetCode 1049.最后一块石头的重量Ⅱ:
思路:
- 01背包(dp为价值)
只要将石头分成重量近似相等的两堆,然后一起粉碎即可得到最小可能重量,从而思路与前面分割子集和相似。
本题中物品重量和物品价值都是stones[i]
动态规划五部曲:
- ① dp数组及下标含义
dp[j]为背包容量为 j 的背包所能装的最大价值 - ② 递推公式:
dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]) - ③ 初始化
j 的范围为[0, sum // 2],dp数组的大小为(sum // 2 + 1),
因为石头的重量都是正整数,因此dp初始化为0,避免初始值影响递推后的结果 - ④ 遍历顺序
先物品后背包容量,且背包容量为逆序遍历 - ⑤ 举例
那么石头堆可以分成重量为dp[target]和(_sum - dp[target])的两堆,因为 _sum // 2是向下取整,因此 _sum - dp[target] > dp[target]
因此相撞之后剩下的最小石头重量为( _sum - dp[target]) - dp[target]
class Solution:
def lastStoneWeightII(self, stones: List[int]) -> int:
_sum = sum(stones)
target = _sum // 2
print("target: " + str(target) + "_sum: " + str(_sum))
dp = [0] * (target + 1) # 初始化为全0
for stone in stones:
for j in range(target, stone - 1, -1):
dp[j] = max(dp[j], dp[j - stone] + stone)
print(dp[target])
return _sum - dp[target] - dp[target]
- 01背包(dp为True or False)
石头堆是否可以分成重量相等的两堆,如果不行,那么去除一小部分(不一定是完整的石头)后是否可以分成重量相等的两堆,此时去除的一小部分就是相撞后剩下的最小石头重量
动态规划五部曲
- ① dp数组及下标含义
dp[j] : 能否从石头堆中取出部分石头,使其重量和恰好为 j - ② 递推
根据 第 i 个物品是取还是不取
如果不取第 i 个物品,dp[j] = dp[j]
如果取第 i 个物品 dp[j] = dp[j - stones[i]]
dp[j]只要上面两个一个成立即可
dp[j] = dp[j] or dp[j - stones[i]] - ③ 初始化
j 的范围为[0, sum // 2],因此dp数组的大小为(sum // 2 + 1)
dp[0]初始化为True,因为石头堆中都不取石头来满足重量和恰好为0,其余非0初始化为False,避免初始值对递推的结果产生影响 - ④ 遍历顺序
01背包的遍历顺序,先物品后背包容量,逆序遍历背包容量 - ⑤ 举例
dp[j] = True表示可以从石头中拿出石头且总重量为j,因此碰撞后最小的石头重量为 _sum - j * 2
class Solution:
def lastStoneWeightII(self, stones: List[int]) -> int:
target_sum = sum(stones)
target = target_sum // 2
dp = [False] * (target + 1)
dp[0] = True
for stone in stones:
for j in range(target, stone - 1, -1):
dp[j] = dp[j] or dp[j - stone]
for j in range(target, -1, -1):
if dp[j]:
return target_sum - 2 * j
return 0
感悟:
怎么将问题转换为01背包问题
LeetCode 494.目标和:
思路:
- 二维动态规划
将题目转换为向背包中放数据
可以将题目中添加了“+”和“-”的数按照“+”和“-”分成两类划归到一起,从而对非负整数数组加符号运算后为target的式子如下:
left - right = target(如 (1 + 1+ 1) - (1 + 1) = 1)
又left + right = sum
可得left = (sum + target) // 2
因此,当abs(target) > sum时是没有方案的
(sum + target) % 2 == 1也是没有方案的。
因此题目可以转换为从数组中取值存放在背包中,一共有多少种办法使得放入背包中的总价值恰好为 j
物品的重量和价值均为nums[i],背包最大重量为left
动态规划五部曲
- ① dp数组及下标含义
dp[i][j]表示从[0, i]中取物品,使得价值总和恰好为 j 的种类为dp[i][j] - ② 递推式
还是根据是否取第 i 个物品来分类
1)如果取第 i 个物品, 那么背包剩下的需要满足的价值为j - nums[i],价值总和恰好为 j 的种类有dp[i - 1][j - nums[i]]
2)如果不取第 i 个物品,价值总和恰好为 j 的种类有dp[i - 1][j]种
递推式为dp[i][j] = dp[i - 1][j] + dp[i - 1][j - nums[i]]
当 j >= nums[i]时成立。
如果 j < nums [i], dp[i][j] = dp[i - 1][j]
- ③ 初始化
由递推式可知,dp[i][j]由dp[i - 1][j]和dp[i - 1][j - nums[i]]得到,即需要左边和上边的数据。因此需要初始化第0行和第0列
初始化第0行,dp[0][0] = 1,
第0行只有 j = nums[i]的方法才为1,其余的要么装不满,要么装过了
dp[0][nums[i]] = 1
初始化第0列
因为非负整数可能出现0,如果有2个0元素,分别为元素1和元素2,那么dp[i][0]的种类应当有:
都不选
选元素1
选元素2
选元素1和2
一共4 = 2** 2种
zero = 0
for i in range(len(nums)):
if nums[i] == 0:
zero += 1
dp[i][0] = 2** zero
- ④ 遍历顺序
因为是二维数组,所以遍历顺序可以先物品后背包容量,也可以先背包容量后物品,且顺序和逆序均可 - ⑤ 举例:
dp[-1][-1]即为题目所要求的数目
class Solution:
def findTargetSumWays(self, nums: List[int], target: int) -> int:
_sum = sum(nums)
if abs(target) > _sum: # 没有方案
return 0
if (target + _sum) % 2 == 1: # 没有方案
return 0
maxj = (target + _sum) // 2
dp = [[0] * (maxj + 1) for _ in range(len(nums))]
# 初始化
if nums[0] <= maxj: # 初始化最上面一行
dp[0][nums[0]] = 1
dp[0][0] = 1
zero = 0 # 初始化最左边一列
for i in range(len(nums)):
if nums[i] == 0:
zero += 1
dp[i][0] = 2 ** zero
# 递推
for i in range(1, len(nums)):
for j in range(1, maxj + 1):
if j >= nums[i]:
dp[i][j] = dp[i - 1][j] + dp[i - 1][j - nums[i]]
else:
dp[i][j] = dp[i - 1][j]
return dp[-1][-1]
- 一维动态规划
实际上和前面的01背包从二维到一维相同,递推式不同。
dp[j] = dp[j] + dp[j - nums[i]]
动态规划五部曲
- ① dp数组及下标含义
dp[j]表示使得价值总和恰好为 j 的种类为dp[j] - ② 递推式
dp[j] = dp[j] + dp[j - nums[i]] - ③ 初始化
dp[0] = 1,其余非0初始化为0避免初始值影响递推的结果 - ④ 遍历顺序
先物品后背包容量,且背包容量为逆序遍历 - ⑤ 举例:
class Solution:
def findTargetSumWays(self, nums: List[int], target: int) -> int:
target_sum = sum(nums)
if abs(target) > target_sum: # 没有方案
return 0
if (target + target_sum) % 2 == 1: # 没有方案
return 0
maxj = (target + target_sum) // 2
dp = [0] * (maxj + 1)
dp[0] = 1 # 初始化
for num in nums:
for j in range(maxj, num - 1, -1):
dp[j] += dp[j - num]
return dp[maxj]
LeetCode 474.一和零:
思路
本题仍然是01背包问题,只是背包容量从一维变为了二维n, m,因此采用二维dp数组分别记录n 和m(相当于前面的dp[j])
物品为字符串,其重量和价值均为其0和1的个数,背包最大容量为n 和 m
动态规划五部曲
- ① dp数组及下标含义
dp[i][j] : strs中最多有 i 个0和 j 个1的最大子集长度 - ② 递推
根据是否选择当前 s 来分类
1)不选择s:dp[i][j] = dp[i][j]
2)选择s,s中0和1的个数分别为zeroNums和oneNums,dp[i][j] = dp[i - zeroNums][ j - oneNums]
因为要求最大子集长度,因此是求max
dp[i][j] = max(dp[i][j], dp[i - zeroNums][ j - oneNums]) - ③ 初始化
dp数组的大小为(n + 1) * (m + 1)(m和n可以交换)。dp[0][0] = 0,因为是求max且0和1的个数最小为0,因此其余非0初始化为0避免初始值影响递推的结果。(可以理解为集合为空集时的dp数组) - ④ 遍历顺序
先遍历物品,即字符串,再逆序遍历背包容量即n, m,其中n和m的遍历先后无所谓,但是要是逆序遍历 - ⑤ 举例
class Solution:
def findMaxForm(self, strs: List[str], m: int, n: int) -> int:
dp = [[0] * (n + 1) for _ in range(m + 1)] # 初始化二维dp数组
# 先遍历物品,再逆序遍历背包
for s in strs:
zeroNums = s.count('0')
oneNums = len(s) - zeroNums
# 逆序遍历背包
for i in range(m, zeroNums - 1, -1):
for j in range(n, oneNums - 1, -1):
dp[i][j] = max(dp[i][j], dp[i - zeroNums][j - oneNums] + 1)
return dp[m][n]
学习收获:
首先将题目转换为01背包问题,如背包最大价值,以及背包最大种类。转换时明确物品、物品重量价值以及背包最大容量应当为多少(比如目标和)
多维度背包容量的解法