继续还债。1049. 最后一块石头的重量 II,494. 目标和,474. 一和零。01背包会了,但是应用不会,就是单独摆明给我一道类似的题目做起来会很快,但是套到某些场景里面就很难看出来怎么用01背包去做。
1049. 最后一块石头的重量 II
1049. 最后一块石头的重量 II - 力扣(LeetCode)
要是不知道这是DP题目,我是绝对想不出来是用DP的。如果不是说这道题和昨天那道分割字数和思路类似的,我也想不出来这是01背包的。知道了这两点,开始面向方法编程,硬往这个方向靠,才想出来题目可以简化为,把所有石头分成重量尽量类似的两堆然后相撞,最后剩下就是最少,如果能分成等重,那就能为0。所以代码前半部分和之前题目类似,最后需要多一步求剩余重量,依旧是倒着遍历,选重量从大到小,最后剩下的重量就少,当为True的时候,返回就行了。return0是一个兜底的情况,实际不会发生。
class Solution:
def lastStoneWeightII(self, stones: List[int]) -> int:
total = sum(stones)
target = total // 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 i in range(target, -1, -1):
if dp[i]:
# 总重量减去两倍的最接近总重量一半的重量
return total - 2 * i
return 0
494. 目标和
勉强看着题解理解懂的一道题,最主要的是自己想破头也不可能想出来这是01背包的问题。首先是要经过一个公式推导,得出类似上题石头碰撞的表达式,就是推我们的target_sum 等于所有值的和+目标值的和然后除以二,让我们通过背包判断出来这样的方案有多少个。
dp的含义是类似于背包的,dp[i][j] = 从前 i+1 个数(即 nums[0..i])中,选若干个数,使得它们的和为 j 的方案数量
创建数组和之前一样,然后初始化第一行,dp[0][0] = 1代表空集就能凑出0,所以方案数为1。然后判断如果第一个数就不超过目标和,那么我们可以选择它一个人单独构成 nums[0]
这个和,方案数为 1
之后是特殊处理0这个值,因为0在数组中可选可不选,但是最终还是会影响方案数,所以需要特殊考虑。遍历所有数字,算出有k个0,那么方案数就是2的k次方。最后状态转移方程类似于01背包的dp[i][j] = dp[i-1][j] + dp[i-1][j - nums[i]]。
(之后还得重新理解一下这道题,太难了)
class Solution:
def findTargetSumWays(self, nums: List[int], target: int) -> int:
"""
先进行一个数学推导,P为正数数组和,N为负数数组和,那么P+N = total, P-N = target
P + N + P - N = total + target
P = (total + target) // 2
"""
total_sum = sum(nums)
if abs(target) > total_sum: # 全是正数也没有target大
return 0
if (target + total_sum) % 2 == 1: # 保证能被整除
return 0
target_sum = (target + total_sum) // 2
# 创建二维动态规划数组,行表示选取的元素数量,列表示累加和
dp = [[0] * (target_sum + 1) for _ in range(len(nums))]
# 初始化状态
dp[0][0] = 1
if nums[0] <= target_sum:
dp[0][nums[0]] = 1
numZero = 0
for i in range(len(nums)):
if nums[i] == 0:
numZero += 1
dp[i][0] = int(math.pow(2, numZero))
for i in range(1, len(nums)):
for j in range(target_sum + 1):
dp[i][j] = dp[i - 1][j] # 不选取当前元素
if j >= nums[i]:
dp[i][j] += dp[i - 1][j - nums[i]] # 选取当前元素
return dp[len(nums)-1][target_sum]