代码随想录第32天:动态规划5(组合、排列、最小方法数)
一、零钱兑换(Leetcode 518)
1、确定dp数组以及下标的含义
使用 下标为[0, i]的coins[i]能够凑满j(包括j)这么大容量的包,有dp[i][j]种组合方法。
2、确定递推公式
dp[i][j] = dp[i - 1][j] + dp[i][j - nums[i]]
3. dp数组初始化
dp[i][0] 的含义:用物品i(即coins[i]) 装满容量为0的背包 有几种组合方法。
都有一种方法,即不装。dp[i][0] 都初始化为1
dp[0][j]的含义:用「物品0」(即coins[0]) 装满 背包容量为j的背包,有几种组合方法。如果 j 可以整除 物品0,那么装满背包就有1种组合方法。
4. 确定遍历顺序
二维DP数组的完全背包的两个for循环先后顺序都可以。
class Solution:def change(self, amount: int, coins: List[int]) -> int:n = len(coins)dp = [[0] * (amount + 1) for _ in range(n + 1)]dp[0][0] = 1 # 0个硬币凑出金额0的方法是1种for i in range(1, n + 1): # 遍历硬币种类for j in range(amount + 1): # 遍历目标金额# 不选当前硬币 coins[i-1]dp[i][j] = dp[i - 1][j]# 选当前硬币(只要j >= coins[i-1])if j >= coins[i - 1]:dp[i][j] += dp[i][j - coins[i - 1]]return dp[n][amount]
一维数组解法:
1、确定dp数组以及下标的含义
dp[j]:凑成总金额j的货币组合数为dp[j]
2、确定递推公式
dp[j] += dp[j - coins[i]]
3. dp数组初始化
装满背包容量为0 的方法是1,即不放任何物品,dp[0] = 1
4. 确定遍历顺序
先物品再背包求的是组合数,先背包后物品求的是排列数!!!
求组合所以本题先物品后背包
from typing import Listclass Solution:def change(self, amount: int, coins: List[int]) -> int:# 初始化一维数组 dp,长度为 amount + 1# dp[j] 表示凑出金额 j 的组合数dp = [0] * (amount + 1)# 凑出金额为 0 只有一种方式:什么也不选dp[0] = 1# 遍历每一个硬币for coin in coins:# 遍历金额从 coin 到 amount# 只从 coin 开始是因为凑不出比硬币面额更小的金额for j in range(coin, amount + 1):# 状态转移:凑出金额 j 的方法数 += 凑出金额 j - coin 的方法数dp[j] += dp[j - coin]# 返回凑出目标金额 amount 的组合方式数return dp[amount]
二、组合总合IV(Leetcode 377)
1、确定dp数组以及下标的含义
dp[i]:凑成目标正整数为i的排列个数为dp[i]
2、确定递推公式
dp[i] += dp[i - nums[j]]
3. dp数组初始化
dp[0] = 1
4. 确定遍历顺序
先物品再背包求的是组合数,先背包后物品求的是排列数!!!
求排列所以本题先背包后物品
from typing import Listclass Solution:def combinationSum4(self, nums: List[int], target: int) -> int:# dp[i] 表示凑出和为 i 的排列个数(顺序不同算不同的组合)dp = [0] * (target + 1)# 凑出和为 0 只有一种方式:什么也不选dp[0] = 1# 从 1 到 target 遍历每一个目标值 ifor i in range(1, target + 1):# 遍历每一个数字 num,尝试使用它来构造 ifor j in range(len(nums)):if i >= nums[j]:# 如果当前目标值 i ≥ num,则可以从 i - num 的方案转移过来dp[i] += dp[i - nums[j]]# 最终结果是凑出目标和 target 的所有排列数return dp[target]
三、爬楼梯(Kamacoder 57)
1、确定dp数组以及下标的含义
dp[i]:爬到有i个台阶的楼顶,有dp[i]种方法。
2、确定递推公式
dp[i]有几种来源,dp[i - 1],dp[i - 2],dp[i - 3] 等等,即:dp[i - j],递推公式为:dp[i] += dp[i - j]
3. dp数组初始化
dp[0] = 1
4. 确定遍历顺序
先物品再背包求的是组合数,先背包后物品求的是排列数!!!
求排列所以本题先背包后物品
def solve(n, m):dp = [0] * (n + 1)dp[0] = 1for j in range(1, n + 1):for i in range(1, m + 1):if j >= i:dp[j] += dp[j - i]return dp[n]if __name__ == '__main__':n, m = list(map(int, input().split(' ')))print(solve(n, m))
四、零钱兑换(Leetcode 322)
1、确定dp数组以及下标的含义
dp[j]:凑足总额为j所需钱币的最少个数为dp[j]
2、确定递推公式
凑足总额为 j - coins[i] 的最少个数为dp[j - coins[i]],那么只需要加上一个钱币coins[i]即dp[j - coins[i]] + 1就是dp[j]
所以dp[j] 要取所有 dp[j - coins[i]] + 1 中最小的。
递推公式:dp[j] = min(dp[j - coins[i]] + 1, dp[j]);
3. dp数组初始化
dp[0] = 0
dp[j]应该初始化为一个最大的数float('inf'),这样在min(dp[j - coins[i]] + 1, dp[j])比较的过程中保持更新
所以下标非0的元素都初始化为最大值float('inf')。
4. 确定遍历顺序
先物品再背包求的是组合数,先背包后物品求的是排列数!!!
本题并不强调集合是组合还是排列,习惯先物品后背包
from typing import Listclass Solution:def coinChange(self, coins: List[int], amount: int) -> int:# 初始化 dp 数组,长度为 amount + 1,初始值为正无穷(代表初始无法到达)# dp[i] 表示凑出金额 i 所需的最少硬币数dp = [float('inf')] * (amount + 1)# 凑出金额 0 所需的硬币数为 0dp[0] = 0# 遍历每一种硬币for coin in coins:# 对每一个从 coin 到 amount 的金额 j 进行遍历for j in range(coin, amount + 1):# 状态转移方程:如果 j - coin 可以被凑出# 则 j 也可以被凑出,其所需硬币数为 dp[j - coin] + 1dp[j] = min(dp[j], dp[j - coin] + 1)# 如果最终凑不出 amount 金额,返回 -1;否则返回最小硬币数return dp[amount] if dp[amount] != float('inf') else -1