一、1049. 最后一块石头的重量 II
本题就和 昨天的 416. 分割等和子集 很像了,可以尝试先自己思考做一做。
1. 具体思路
将石头分为近似大小和重量的两堆,最后两两相撞最多只剩下一块石头。
(1)dp数组的含义:装满容量为j的背包它的最大重量为dp[j]。
(2)递推公式:本题中石头的重量就是石头的价值,递推公式为dp[j] = max(dp[j], dp[j-stones[i]]+stones[i])
(3)dp数组初始化:dp[0]=0,容量为0所以重量也为0。非0下标也初始化为0,因为必须为非负,且后续需要比较最大值,所以只能初始化为0。此处需要注意定义dp数组的大小,由于题目描述为1 <= stones.length <= 30且1 <= stones[i] <= 100,所以30*100/2=1500,定义dp数组大小为1500即可。
(4)遍历顺序:第一层for循环遍历的是每个石头;第二层遍历的是背包,并且需要从大到小进行遍历,保证每个物品只放一次;target就是sum/2,凑这样大的一个背包,并且第二层需要保证当前容量大于石头。
(5)打印dp数组。
2. 代码实现
class Solution:
def lastStoneWeightII(self, stones: List[int]) -> int:
dp = [0] * 15001
total_sum = sum(stones)
target = total_sum // 2
for i in stones:
for j in range(target, i-1, -1):
dp[j] = max(dp[j], dp[j-i]+i)
return total_sum - dp[target]-dp[target]
二、494. 目标和
大家重点理解 递推公式:dp[j] += dp[j - nums[i]],这个公式后面的提问 我们还会用到。
1. 具体思路
(1)定义dp数组:装满容量为 j 的背包有dp[ j ]种方法。
(2)递归公式:dp[ j ] += dp[ j-nums[i]]
(3)dp数组初始化:dp[0] = 1,不可以初始化为0,因为递推公式会一直是0。非0下标初始化为0即可,因为需要进行累加操作,如果初始为非0 ,那么会影响递归公式的推导。
(4)遍历顺序:第一层遍历物品,第二层遍历背包容量(倒序),因为每个背包只能使用一次,所以是倒序。
(5)打印dp数组。
2. 代码实现
- 计算总和
total_sum
:- 计算数组
nums
的总和是正确的第一步,用于后续判断。
- 计算数组
- 提前返回0的情况:
- 如果
abs(target)
大于total_sum
,则不可能通过选取数组中的元素来达到目标值(考虑正负情况)。 - 如果
(target + total_sum) % 2 == 1
,意味着目标和为奇数,而数组中的元素总和为偶数(或反之),那么也无法通过选取元素达到目标值(因为选取元素相当于做加减运算,无法改变总和的奇偶性)。
- 如果
- 目标和
target_sum
:- 这里的
target_sum
实际上是将问题转化为背包问题中的“不超过某个目标和的最大值问题”。因为target
可以是负数,所以通过target + total_sum
将其转化为非负问题,然后除以2得到我们需要达到的一半和(因为我们可以选择一部分元素使其和为某个值,剩下的元素和就是total_sum
减去这个值,两者和等于total_sum + target
,即目标和为(target + total_sum) // 2
)。
- 这里的
- 动态规划数组
dp
:dp[j]
表示选取若干元素和为j
的方案数。- 初始化
dp[0] = 1
是正确的,表示和为0(即不选取任何元素)的方案数为1。
- 状态转移方程:
dp[j] += dp[j - num]
表示在已经考虑了和为j-num
的方案数基础上,加上当前元素num
可以得到和为j
的方案数。- 注意循环的范围,这里应该是
range(target_sum, num - 1, -1)
改为range(target_sum, num - 1, -1)
实际上是有问题的,因为当j = num - 1
时,dp[j - num]
会访问dp[-1]
,这是越界的。正确的范围应该是range(target_sum, num - 1, -1)
改为range(target_sum, num - 1, -1)
的上界调整为num
(包含num
),即range(target_sum, num - 1, -1)
改为range(target_sum, num, -1)
。但更常见的写法是从target_sum
到num
(不包含target_sum - num < 0
的情况),即range(max(target_sum - num, 0), target_sum + 1)
。不过由于j
从target_sum
开始递减,且我们关心的是dp[j]
的更新不依赖于未来的dp[j+k]
(k>0),所以只需要保证不越界即可。
class Solution:
def findTargetSumWays(self, nums: List[int], target: int) -> int:
sum_total = sum(nums)
target_sum = (sum_total + target) // 2
if abs(target) > sum_total:
return 0
if (target + sum_total) % 2 == 1:
return 0
dp = [0] * (target_sum+1)
dp[0] = 1
for i in nums:
for j in range(target_sum, i-1, -1):
dp[j] += dp[j-i]
return dp[target_sum]
三、474.一和零
通过这道题目,大家先粗略了解, 01背包,完全背包,多重背包的区别,不过不用细扣,因为后面 对于 完全背包,多重背包 还有单独讲解。
1. 具体思路
首先这道题属于0-1背包的题目,而不是多重背包,只限制了一个背包最多装多少种物品。
(1)定义dp数组及初始化:初始化一个二维动态规划数组 dp,大小为 (m+1) x (n+1),dp[i][j] 表示在至多有 i 个 '0' 和 j 个 '1' 的条件下,可以选择的最大字符串数量;
(2)遍历字符串数组 strs 中的每一个字符串 s,统计字符串 s 中 '0' 的数量,统计字符串 s 中 '1' 的数量;
(3)遍历顺序这里的 range 条件应该包含 zero 和 one,以确保考虑到当前字符串 s,但是从后向前遍历(逆序)是为了避免重复使用同一个字符串 s;
(4)dp数组递推公式:dp[i][j] = max(dp[i][j], dp[i-zero][j-one] + 1)
(5)打印dp数组:返回最终结果,在至多有 m 个 '0' 和 n 个 '1' 的条件下,可以选择的最大字符串数量,return dp[m][n]。
2. 代码实现
class Solution:
def findMaxForm(self, strs: List[str], m: int, n: int) -> int:
dp = [[0]*(n+1) for _ in range(m+1)]
for s in strs:
one = s.count('1')
zero = s.count('0')
for i in range(m, zero-1, -1):
for j in range(n, one-1, -1):
dp[i][j] = max(dp[i][j], dp[i-zero][j-one]+1)
return dp[m][n]