代码随想录训练营 Day37打卡 动态规划 part05
一、完全背包理论基础
有N件物品和一个最多能背重量为W的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品都有无限个(也就是可以放入背包多次),求解将哪些物品装入背包里物品价值总和最大。
完全背包和01背包问题唯一不同的地方就是,每种物品有无限件。
首先再回顾一下01背包的核心代码
for(int i = 0; i < weight.size(); i++) { // 遍历物品
for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
我们知道01背包内嵌的循环是从大到小遍历,为了保证每个物品仅被添加一次。
而完全背包的物品是可以添加多次的,所以要从小到大去遍历,即:
// 先遍历物品,再遍历背包
for(int i = 0; i < weight.size(); i++) { // 遍历物品
for(int j = weight[i]; j <= bagWeight ; j++) { // 遍历背包容量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
二、力扣518. 零钱兑换II
给你一个整数数组 coins 表示不同面额的硬币,另给一个整数 amount 表示总金额。
请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0 。
假设每一种面额的硬币有无限个。
题目数据保证结果符合 32 位带符号整数。
示例 :
输入:amount = 5, coins = [1, 2, 5]
输出:4
解释:有四种方式可以凑成总金额:
5=5
5=2+2+1
5=2+1+1+1
5=1+1+1+1+1
本题要求凑成总和的组合数,元素之间明确要求没有顺序。
那么本题,两个for循环的先后顺序可就有说法了。
我们先来看 外层for循环遍历物品(钱币),内层for遍历背包(金钱总额)的情况。
for (int i = 0; i < coins.size(); i++) { // 遍历物品
for (int j = coins[i]; j <= amount; j++) { // 遍历背包容量
dp[j] += dp[j - coins[i]];
}
}
假设:coins[0] = 1,coins[1] = 5。
那么就是先把1加入计算,然后再把5加入计算,得到的方法数量只有{1, 5}这种情况。而不会出现{5, 1}的情况。
所以这种遍历顺序中dp[j]里计算的是组合数!
如果把两个for交换顺序,代码如下:
for (int j = 0; j <= amount; j++) { // 遍历背包容量
for (int i = 0; i < coins.size(); i++) { // 遍历物品
if (j - coins[i] >= 0) dp[j] += dp[j - coins[i]];
}
}
背包容量的每一个值,都是经过 1 和 5 的计算,包含了{1, 5} 和 {5, 1}两种情况。
此时dp[j]里算出来的就是排列数!
输入: amount = 5, coins = [1, 2, 5] ,dp状态图如下:
最后红色框dp[amount]为最终结果。
在求装满背包有几种方案的时候,认清遍历顺序是非常关键的。
如果求组合数就是外层for循环遍历物品,内层for遍历背包。
如果求排列数就是外层for遍历背包,内层for循环遍历物品。
代码实现
class Solution:
def change(self, amount: int, coins: List[int]) -> int:
# 初始化 dp 数组,其中 dp[i] 表示金额为 i 时的硬币组合数
dp = [0] * (amount + 1)
# 当金额为 0 时,只有一种方式(即不选择任何硬币)
dp[0] = 1
# 遍历每一种硬币面额
for i in range(len(coins)):
# 对于当前硬币面额 coins[i],遍历从 coins[i] 到 amount 的所有金额
# 这样确保计算 dp[j] 时,dp[j - coins[i]] 已经计算完毕,符合动态规划的要求
for j in range(coins[i], amount + 1):
# 更新 dp[j],将当前面额的硬币考虑进来
dp[j] += dp[j - coins[i]]
# dp[j] 表示在考虑当前硬币后,凑成金额 j 的组合数
# dp[j - coins[i]] 表示凑成金额 (j - 当前硬币面额) 的组合数
# 将其加入 dp[j],表示加上当前硬币后,组合数的增加
# 返回凑成总金额 amount 的硬币组合数
return dp[amount]
三、力扣377. 组合总和 Ⅳ
给你一个由 不同 整数组成的数组 nums ,和一个目标整数 target 。请你从 nums 中找出并返回总和为 target 的元素组合的个数。
题目数据保证答案符合 32 位整数范围。
示例 :
输入: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)
请注意,顺序不同的序列被视作不同的组合。
本质是本题求的是排列总和,而且仅仅是求排列总和的个数,并不是把所有的排列都列出来。
dp[i]: 凑成目标正整数为i的排列个数为dp[i]
dp[i](考虑nums[j])可以由 dp[i - nums[j]](不考虑nums[j]) 推导出来。
因为只要得到nums[j],排列个数dp[i - nums[j]],就是dp[i]的一部分。
在动态规划:494.目标和 (opens new window)和 动态规划:518.零钱兑换II (opens new window)中我们已经讲过了,求装满背包有几种方法,递推公式一般都是dp[i] += dp[i - nums[j]];
本题也一样。
如果求排列数就是外层for遍历背包,内层for循环遍历物品。
所以本题遍历顺序最终遍历顺序:target(背包)放在外循环,将nums(物品)放在内循环,内循环从前到后遍历。
我们再来用示例中的例子推导一下:
代码实现
class Solution:
def combinationSum(self, nums: List[int], target: int) -> int:
# 初始化 dp 数组,dp[i] 表示凑成目标整数 i 的排列组合个数
dp = [0] * (target + 1)
# 当目标为 0 时,只有一种组合方式,即不选择任何元素
dp[0] = 1
# 遍历所有可能的目标数值 i,从 1 到 target
for i in range(1, target + 1): # 遍历背包容量(目标数值)
# 遍历每个元素 nums[j]
for j in range(len(nums)): # 遍历物品(数组中的每个元素)
# 如果当前目标 i 减去 nums[j] 之后,仍然是非负数
# 说明 nums[j] 可以作为一个组成部分加入当前的排列组合中
if i - nums[j] >= 0:
# 更新 dp[i],dp[i] 通过 dp[i - nums[j]] 转移而来
# dp[i - nums[j]] 是之前的目标值为 i - nums[j] 时的组合数
# 加入 nums[j] 后,这些组合可以形成目标 i
dp[i] += dp[i - nums[j]]
# 返回凑成目标整数 target 的排列组合个数
return dp[target]
四、卡码网70. 爬楼梯(进阶版)
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬至多m (1 <= m < n)个台阶。你有多少种不同的方法可以爬到楼顶呢?
注意:给定 n 是一个正整数。
示例 :
输入描述:输入共一行,包含两个正整数,分别表示n, m
输出描述:输kama出一个整数,表示爬到楼顶的方法数。
输入:3 2
输出:3
提示信息:
数据范围:
1 <= m < n <= 32;
当 m = 2,n = 3 时,n = 3 这表示一共有三个台阶,m = 2 代表你每次可以爬一个台阶或者两个台阶。
此时你有三种方法可以爬到楼顶。
1 阶 + 1 阶 + 1 阶段
1 阶 + 2 阶
2 阶 + 1 阶
这其实是一个完全背包问题。1阶,2阶,… m阶就是物品,楼顶就是背包。
每一阶可以重复使用,例如跳了1阶,还可以继续跳1阶。
问跳到楼顶有几种方法其实就是问装满背包有几种方法。
此时大家应该发现这就是一个完全背包问题了!
dp[i]:爬到有i个台阶的楼顶,有dp[i]种方法。
本题呢,dp[i]有几种来源,dp[i - 1],dp[i - 2],dp[i - 3] 等等,即:dp[i - j]
那么递推公式为:dp[i] += dp[i - j]
这是背包里求排列问题,即:1、2 步 和 2、1 步都是上三个台阶,但是这两种方法不一样!
所以需将target放在外循环,将nums放在内循环。
代码实现
def climbing_stairs(n, m):
# 初始化 dp 数组,dp[i] 表示爬到 i 个台阶有 dp[i] 种方法
dp = [0] * (n + 1) # 楼梯一共有 n 个台阶,索引从 0 到 n
dp[0] = 1 # 爬到 0 阶的方法只有一种,即不爬
# 遍历背包容量(即总台阶数)
for j in range(1, n + 1): # j 表示当前目标台阶数,从 1 到 n
# 遍历每一种台阶步数(即可以选择的步数)
for i in range(1, m + 1): # i 表示每次可以爬的台阶数,从 1 到 m
if j >= i: # 如果当前目标台阶数 j 大于等于步数 i
dp[j] += dp[j - i] # 累加 dp[j - i],因为从 j - i 爬到 j 是一种可能的方案
return dp[n] # 返回爬到 n 阶的方法数
if __name__ == '__main__':
# 从输入中获取 n 和 m
n, m = list(map(int, input().split(' ')))
# 打印结果,即爬到 n 阶的方法数
print(climbing_stairs(n, m))