力扣hot100 | 动态规划1 | 70. 爬楼梯、118. 杨辉三角、198. 打家劫舍、279. 完全平方数、322. 零钱兑换
70. 爬楼梯
力扣题目链接
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
示例 1:
输入:n = 2
输出:2
解释:有两种方法可以爬到楼顶。
1. 1 阶 + 1 阶
2. 2 阶
示例 2:
输入:n = 3
输出:3
解释:有三种方法可以爬到楼顶。
1. 1 阶 + 1 阶 + 1 阶
2. 1 阶 + 2 阶
3. 2 阶 + 1 阶
一、递归 + 记录返回值 = 记忆化搜索
参见灵茶山艾府
class Solution:def climbStairs(self, n: int) -> int:@cache # 缓存装饰器,避免重复计算 dfs 的结果—— 这行不写会超时def dfs(i: int) -> int:if i <= 1:return 1return dfs(i - 1) + dfs(i - 2)return dfs(n)
加速前
- 时间复杂度 O(2n2^n2n)。搜索树可以近似为一棵二叉树,树高为 O(n),所以节点个数为 O(2n2^n2n),遍历搜索树需要 O(2n2^n2n)的时间。
- 空间复杂度 O(n)。递归需要 O(n) 的栈空间。
加速后
- 时间复杂度 O(n)。由于每个状态只会计算一次,动态规划的时间复杂度 = 状态个数 × 单个状态的计算时间。本题状态个数等于 O(n),单个状态的计算时间为 O(1),所以动态规划的时间复杂度为 O(n)。
- 空间复杂度 O(n)。有多少个状态,
memo
数组的大小就是多少。
二、递推
dp[i]
定义:从0
爬到i
层(爬了i
层)的方案数。所以答案就是dp[n]
。- 递推公式:
dp[i] = dp[i - 1] + dp[i - 2]
- 初始化:
dp[1] = 1,dp[2] = 2
(若要dp[0] = dp[1] = 1
,则遍历可从i=2
开始,但相当于强加了dp[0]
的意义)【但选择有意义的初始化方法的话,一定不能忘记在开头特判n==1
,不然初始化dp[2]
会报错】 - 遍历【方向+边界】:从左到右、从
dp[3]
开始、到dp[n]
结束。
class Solution:def climbStairs(self, n: int) -> int:if n == 1: # 别忘这个!!return ndp = [0] * (n+1)dp[1] = 1dp[2] = 2 # 不然这里会报错for i in range(3, n+1): # 这里却不会,因为range()中就算输入不合法的值,也只是返回 空的 range 对象dp[i] = dp[i-1] + dp[i-2]return dp[n]
- 时间复杂度 O(n)
- 空间复杂度 O(n)
三、空间优化
其实只需维护三个整数即可:
class Solution:def climbStairs(self, n: int) -> int:if n == 1: return 1dp1 = 1dp2 = 2 for i in range(3, n+1): dp3 = dp1 + dp2dp1, dp2 = dp2, dp3return dp2
- 时间复杂度 O(n)
- 空间复杂度 O(1)
118. 杨辉三角
力扣题目链接
给定一个非负整数 numRows
,生成「杨辉三角」的前numRows
行。
在「杨辉三角」中,每个数是它左上方和右上方的数的和。
示例 1:
输入: numRows = 5
输出: [[1],[1,1],[1,2,1],[1,3,3,1],[1,4,6,4,1]]
示例 2:
输入: numRows = 1
输出: [[1]]
class Solution:def generate(self, numRows: int) -> List[List[int]]:res = [[1] * (i + 1) for i in range(numRows)]for i in range(2, numRows): # 前两行都是1不用改for j in range(1, i): # 第i行有i+1列(末尾是第i列),但不用改第i列res[i][j] = res[i - 1][j - 1] + res[i - 1][j]return res
- 时间复杂度 O(numRows2)O(numRows^2)O(numRows2)
- 空间复杂度 O(1)
198. 打家劫舍
力扣题目链接
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
示例 1:
输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
示例 2:
输入:[2,7,9,3,1]
输出:12
解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
偷窃到的最高金额 = 2 + 9 + 1 = 12 。
一、递归 + 记忆缓存
from functools import cache # python3.9开始
# from functools import lru_cache as cache # 3.9以下的版本
class Solution:def rob(self, nums: List[int]) -> int:@cachedef dfs(i): # dfs(i) 表示从 nums[0] 到 nums[i] 最多能偷多少if i < 0:return 0# 不偷i 偷i return max(dfs(i-1), dfs(i-2) + nums[i])return dfs(len(nums) - 1)
- 时间复杂度 O(n)
- 空间复杂度 O(n)
二、递推
参考自灵茶山艾府
dp数组
class Solution:def rob(self, nums: List[int]) -> int:f = [0] * (len(nums) + 2)for i, x in enumerate(nums):f[i + 2] = max(f[i + 1], f[i] + x)return f[-1]
- 时间复杂度 O(n)
- 空间复杂度 O(n)
空间优化
class Solution:def rob(self, nums: List[int]) -> int:f0 = f1 = 0for x in nums:f0, f1 = f1, max(f1, f0 + x)return f1
- 时间复杂度 O(n)
- 空间复杂度 O(1)
279. 完全平方数【完全背包】
力扣题目链接
给你一个整数 n
,返回 和为 n
的完全平方数的最少数量 。
完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。
示例 1:
输入:n = 12
输出:3
解释:12 = 4 + 4 + 4
示例 2:
输入:n = 13
输出:2
解释:13 = 4 + 9
参考自灵茶山艾府。
递归
- 【注意几点】:
- 要把记忆化搜索的
memo
数组声明为全局变量,这样可以在多个测试数据之间共享,从而减少计算量。Python 可以把dfs
写在类外面。若跟之前一样写在类中,力扣会因超出内存限制而失败! - 为什么本题的递归边界是
i=0
?之前的那些 DP 题的递归边界都是i<0
。——本题最小的完全平方数是 121^212 ,递归到i=0
就说明所有完全平方数都考虑完了。
(其他题目最小的数一般是下标为0
的数,递归到i < 0
才说明所有的数都考虑完了。)
- 要把记忆化搜索的
import math
@cache
def dfs(i, c): #表示第1~i(包括i)个完全平方数中任选、恰好和为n的,最少数量if i == 0:return 0 if c == 0 else float('inf') # 求最小值if c < i ** 2: # 【注意】第i(1-based)个平方数是i^2return dfs(i-1, c)return min(dfs(i-1, c), dfs(i, c-i**2) + 1)class Solution:def numSquares(self, n: int) -> int:# 递归入口:i^2 <= n才不会inf,所以i从不大于 根号n 开始取就可以# math.isqrt()返回不超过 math.sqrt(n) 的最大整数(向下取整)return dfs(math.isqrt(n), n)
- 时间复杂度 O(n⋅nn\cdot \sqrt nn⋅n),由于每个状态只会计算一次,动态规划的时间复杂度=状态个数×单个状态的计算时间动态规划的时间复杂度 = 状态个数 × 单个状态的计算时间动态规划的时间复杂度=状态个数×单个状态的计算时间。本题状态个数等于O(n⋅nn\cdot \sqrt nn⋅n),单个状态的计算时间为 O(1)。
- 空间复杂度 O(n⋅nn\cdot \sqrt nn⋅n)。保存多少状态,就需要多少空间。
322. 零钱兑换【完全背包】
力扣题目链接
给你一个整数数组 coins
,表示不同面额的硬币;以及一个整数 amount
,表示总金额。
计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回-1
。
你可以认为每种硬币的数量是无限的。
示例 1:
输入:coins = [1, 2, 5], amount = 11
输出:3
解释:11 = 5 + 5 + 1
示例 2:
输入:coins = [2], amount = 3
输出:-1
示例 3:
输入:coins = [1], amount = 0
输出:0
递归
from functools import cache
class Solution:def coinChange(self, coins: List[int], amount: int) -> int:@cachedef dfs(i, c): # 代表[0:i](包括i)硬币中任选,刚好组成c的,最少硬币个数if i < 0:return 0 if c == 0 else float('inf') # 容量为0时只需0个硬币,否则返回不干扰min()取值的infif c < coins[i]: # 容量不够不能选return dfs(i-1, c)# 不选 本轮选一个i(下轮还能选i)return min(dfs(i-1, c), dfs(i, c-coins[i]) + 1) # 求硬币数,所以+1res = dfs(len(coins) - 1, amount)return res if res != float('inf') else -1 # 如例2,最后min(inf, inf+1)不合法
- 时间复杂度 O(n · amount),其中 n 为 coins 的长度。
- 空间复杂度 O(n · amount)