70. 爬楼梯 (进阶)
动态规划解法:
-
定义状态:
dp[i]
表示到达第i
阶楼梯的方法总数。
-
初始条件:
dp[1] = 1
,只有一种方法爬一阶楼梯(即直接爬一阶)。dp[2] = 2
,有两种方法爬两阶楼梯(一次爬两阶或分两次每次爬一阶)。
-
状态转移方程:
dp[i] = dp[i - 1] + dp[i - 2]
,可以从第i-1
阶爬一阶到达,或者从第i-2
阶爬两阶到达。
-
计算方法:
- 从
3
到n
遍历,应用状态转移方程计算每一阶的方法数。
- 从
-
优化:
- 由于每次计算只依赖前两个状态,可以不使用数组来保存所有状态,只需使用两个变量来保存前两个状态,进一步降低算法的空间复杂度。
class Solution:
def climbStairs(self, n: int) -> int:
if n == 1:
return 1
first, second = 1, 2
for i in range(3, n + 1):
third = first + second
first, second = second, third
return second
在这个代码中,first
和 second
分别代表达到当前楼梯前两阶的方法数。循环开始时,first
和 second
分别初始化为达到第一阶和第二阶的方法数。然后,通过迭代更新这两个变量,直到计算出达到第 n
阶的方法数。
322. 零钱兑换
"322. 零钱兑换"问题是一个经典的动态规划问题,类似于完全背包问题。在这个问题中,你需要找到最少的硬币数目,使得它们的总金额等于给定的数额。如果没有任何一种硬币组合能组成总金额,返回 -1
。
问题描述如下:
给定不同面额的硬币 coins
和一个总金额 amount
。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1
。
示例:
输入: coins = [1, 2, 5], amount = 11
输出: 3
解释: 11 = 5 + 5 + 1
在这个问题中,硬币的面额和数量是无限的,类似于完全背包问题中物品的无限性。
解题步骤
-
定义状态:
- 创建一个数组
dp
,其中dp[i]
表示组成金额i
所需的最少硬币个数。
- 创建一个数组
-
初始化状态:
- 初始化
dp[0] = 0
,因为组成金额0不需要任何硬币。 - 其他的
dp[i]
初始化为一个大数,表示无法达到的状态(例如amount + 1
或者float('inf')
)。
- 初始化
-
状态转移方程:
- 遍历每一种硬币面额
coin
,对于每个金额i
从coin
到amount
,更新dp[i]
的值: dp[i] = min(dp[i], dp[i - coin] + 1)
。这里的dp[i - coin] + 1
表示当前金额减去硬币面额后的最少硬币数加上这一枚硬币。
- 遍历每一种硬币面额
-
迭代更新
dp
数组:- 对于
coins
中的每个硬币,我们正序遍历dp
数组从coin
到amount
。
- 对于
-
返回结果:
- 如果
dp[amount]
没有被更新过,说明没有合适的硬币组合能组成总金额,返回-1
。 - 否则,返回
dp[amount]
。
- 如果
代码实现
class Solution:
def coinChange(self, coins: List[int], amount: int) -> int:
dp = [float('inf')] * (amount + 1)
dp[0] = 0
for coin in coins:
for i in range(coin, amount + 1):
dp[i] = min(dp[i], dp[i - coin] + 1)
return dp[amount] if dp[amount] != float('inf') else -1
在这段代码中,我们通过不断迭代更新 dp
数组来找出达到每个金额所需的最少硬币数量。最终 dp[amount]
将给出组成总金额所需的最少硬币数。如果 dp[amount]
仍然是初始化的大数值,意味着没有解,因此返回 -1
。
279.完全平方数
“279. 完全平方数” 是一个经典的动态规划问题。问题描述如下:
给定一个正整数 n
,找到若干个完全平方数(例如,1, 4, 9, 16, …)使得它们的和等于 n
。你需要找到组成和为 n
的完全平方数的最小数量。
示例:
输入: n = 12
输出: 3
解释: 12 = 4 + 4 + 4.
输入: n = 13
输出: 2
解释: 13 = 4 + 9.
解题步骤
-
定义状态:
- 创建一个数组
dp
,其中dp[i]
表示组成数字i
所需的最小完全平方数的数量。
- 创建一个数组
-
初始化状态:
dp[0] = 0
,因为数字0不需要任何平方数。- 其他
dp[i]
初始化为i
,因为最坏的情况是每次加1,所以最多需要i
个数。
-
状态转移方程:
- 对于每个
i
从1
到n
,计算所有小于或等于i
的平方数j*j
,并更新dp[i]
的值: dp[i] = min(dp[i], dp[i - j*j] + 1)
。这里dp[i - j*j] + 1
表示当前数字减去一个完全平方数后的最小平方数数量加上这一个完全平方数。
- 对于每个
-
计算顺序:
- 正序遍历每个数字
i
,确保在计算dp[i]
之前,所有小于i
的dp
值都已经被计算过。
- 正序遍历每个数字
-
优化空间复杂度:
- 实际上,没有必要优化空间复杂度,因为我们需要所有之前的状态来计算当前状态。
代码实现
class Solution:
def numSquares(self, n: int) -> int:
dp = [float('inf')] * (n+1)
dp[0] = 0
# 预计算所有可能用到的平方数
squares = [i*i for i in range(1, int(n**0.5)+1)]
for i in range(1, n+1):
for square in squares:
if i >= square:
dp[i] = min(dp[i], dp[i-square] + 1)
return dp[n]
在这段代码中,dp
数组用来存储达到每个数字所需的最小平方数数量。我们首先计算出所有可能用到的平方数以避免重复计算。然后通过动态规划的方法,考虑减去每个平方数之后的状态,找到最小的可能值。最终 dp[n]
将给出组成数字 n
所需的最小完全平方数数量。如果没有平方数可以组成 n
,这个方法会返回 dp[n]
为 float('inf')
,但在初始化的时候我们已经保证了每个数都能被表示成若干个平方数的和。