2023.3.31总结
做题要领
- 明确dp矩阵和下标i的含义,dp[i]表示的是什么?
- 明确状态转移矩阵
- 明确dp矩阵的初始状态
- 明确遍历的顺序
- 检查方法:推到出dp矩阵的值,打印出dp矩阵的值,看看是否和设想的值一致
遍历顺序
01背包、组合
1、遍历方式讲解
for i in range(len(nums)):
for j in range(n,nums[i]-1,-1):
倒序遍历是为了保证物品i只被放入一次!。但如果一旦正序遍历了,那么物品0就会被重复加入多次!
举一个例子:物品0的重量weight[0] = 1,价值value[0] = 15
如果正序遍历
dp[1] = dp[1 - weight[0]] + value[0] = 15
dp[2] = dp[2 - weight[0]] + value[0] = 30
此时dp[2]就已经是30了,意味着物品0,被放入了两次,所以不能正序遍历。
为什么倒序遍历,就可以保证物品只放入一次呢?
倒序就是先算dp[2]
dp[2] = dp[2 - weight[0]] + value[0] = 15 (dp数组已经都初始化为0)
dp[1] = dp[1 - weight[0]] + value[0] = 15
所以从后往前循环,每次取得状态不会和之前取得状态重合,这样每种物品就只取一次了。
给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。 示例 1:
输入:nums = [1,5,11,5] 输出:true 解释:数组可以分割成 [1, 5, 5] 和 [11] 。
class Solution:
def canPartition(self, nums: List[int]) -> bool:
if sum(nums)%2==1:
return False
n=sum(nums)//2
dp=[0 for i in range(n+1)]
for i in range(len(nums)):
for j in range(n,nums[i]-1,-1):
dp[j]=max(dp[j],dp[j-nums[i]]+nums[i])
return dp[-1]==n
根据动态规划的五部曲来分析一下:
- 首先,分析问题类型,这是01背包问题,因为数组nums中的元素不可以重复选取,且是组合问题不是排序问题
- dp数组的含义是,dp[j]表示背包容量是j时候能够最多装nums中物品的重量。[0, 1, 1, 1, 1, 5, 6, 6, 6, 6, 10, 11]
- 状态转移方程:dp[j]=max(dp[j],dp[j-nums[i]]+nums[i]),数组分组题目等效于,物品质量就是物品价值的01背包问题
- 初始化,全0即可
- 遍历顺序,由于组合问题,物品在外层循环
完全背包、组合
1、遍历方式讲解
for i in range(len(nums)):
for j in range(1,n+1):
if j>=nums[i]:
这个思路就是上面“ 01背包、组合”中讲解的,如果是正序就会导致物品被重复取用,就是完全背包问题
2、例题
518. 零钱兑换 II
给定不同面额的硬币和一个总金额。写出函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个。 示例 1: 输入: amount
= 5, coins = [1, 2, 5] 输出: 4
class Solution:
def change(self, amount: int, coins: List[int]) -> int:
dp=[0 for i in range(amount+1)]
dp[0]=1
for i in range(len(coins)):
for j in range(coins[i],amount+1):
dp[j]+=dp[j-coins[i]]
return dp[-1]
完全背包、排序
1、遍历方式讲解
for j in range(0,target+1):
for i in range(len(nums)):
if j>=nums[i]:
如果求组合数就是外层for循环遍历物品,内层for遍历背包。 如果求排列数就是外层for遍历背包,内层for循环遍历物品。
如果把遍历nums(物品)放在外循环,遍历target的作为内循环的话,举一个例子:计算dp[4]的时候,结果集只有 {1,3}
这样的集合,不会有{3,1}这样的集合,因为nums遍历放在外层,3只能出现在1后面!
所以本题遍历顺序最终遍历顺序:target(背包)放在外循环,将nums(物品)放在内循环,内循环从前到后遍历。
2、例题
377. 组合总和 Ⅳ
给你一个由 不同 整数组成的数组 nums ,和一个目标整数 target 。请你从 nums 中找出并返回总和为 target 的元素组合的个数。
题目数据保证答案符合 32 位整数范围。
示例 1:
输入:nums = [1,2,3], target = 4
输出:7
解释:
所有可能的组合为:
(1, 1, 1, 1)
(1, 1, 2)
(1, 2, 1)
(1, 3)
(2, 1, 1)
(2, 2)
(3, 1)
请注意,顺序不同的序列被视作不同的组合。
class Solution:
def combinationSum4(self, nums: List[int], target: int) -> int:
dp=[0 for i in range(target+1)]
dp[0]=1
for j in range(0,target+1):
for i in range(len(nums)):
if j>=nums[i]:
dp[j]+=dp[j-nums[i]]
return dp[-1]
递推公式
求最小值
dp[j]=min(dp[j],dp[j-i**2]+1)
初始化和求最大值不一样,求最大值直接全初始化为0,但是最小值为[0,最大值,最大值,…,最大值]
dp=[n for i in range(n+1)]
dp[0]=0
给你一个整数 n ,返回 和为 n 的完全平方数的最少数量 。 完全平方数
是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。
例如,1、4、9 和 16 都是完全平方数,而 3 和 11不是。
示例 1:
输入:n = 12
输出:3 解释:12 = 4 + 4 + 4
from math import sqrt
class Solution:
def numSquares(self, n: int) -> int:
dp=[n for i in range(n+1)]
dp[0]=0
a=ceil(sqrt(n))
for i in range(1,a+1):
for j in range(i**2,n+1):
dp[j]=min(dp[j],dp[j-i**2]+1)
return dp[-1]
求最大值
dp[j]=max(dp[j],dp[j-nums[i]]+nums[i])
初始化为[0,0,0,…,0]
416. 分割等和子集
给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
示例 1: 输入:nums = [1,5,11,5]
输出:true 解释:数组可以分割成 [1, 5, 5] 和 [11]
这里取11为背包容量,看看背包能否装满, [1, 5, 5] 将背包装满了,剩下的正好是11,则可以分为两半。
求组合数
dp[j]+=dp[j-nums[i]]
初始化为[1,0,0,…,0]
给你一个由 不同 整数组成的数组 nums ,和一个目标整数 target 。请你从 nums 中找出并返回总和为 target 的元素组合的个数。
题目数据保证答案符合 32 位整数范围。
示例 1:
输入:nums = [1,2,3], target = 4
输出:7
解释:
所有可能的组合为:
(1, 1, 1, 1)
(1, 1, 2)
(1, 2, 1)
(1, 3)
(2, 1, 1)
(2, 2)
(3, 1)
请注意,顺序不同的序列被视作不同的组合。
class Solution:
def combinationSum4(self, nums: List[int], target: int) -> int:
dp=[0 for i in range(target+1)]
dp[0]=1
for j in range(0,target+1):
for i in range(len(nums)):
if j>=nums[i]:
dp[j]+=dp[j-nums[i]]
return dp[-1]