【小白笔记】爬楼梯(Climbing Stairs)
收到了,(Climbing Stairs)是动态规划入门的经典问题。您提到“进阶优化”,通常指的是在标准问题的基础上,考虑,或者要求。我们先回顾标准问题,然后重点分析两种“进阶优化”的思路。
收到了,爬楼梯(Climbing Stairs)是动态规划入门的经典问题。您提到“进阶优化”,通常指的是在标准问题的基础上,考虑更复杂的步长限制,或者要求空间复杂度的极致优化。
我们先回顾标准问题,然后重点分析两种“进阶优化”的思路。
💡 题目分析:爬楼梯(Climbing Stairs)
1. 标准问题回顾
问题: 假设你正在爬楼梯。楼梯有 nnn 级台阶。每次你可以爬 1 级或者 2 级台阶。问:有多少种不同的方法可以爬到楼顶?
核心思路: 这是一个典型的斐波那契(Fibonacci)数列问题。
- 到第 nnn 级台阶的方法数 DP[n]DP[n]DP[n]:
- 只能从第 n−1n-1n−1 级爬 1 步上来。
- 或者从第 n−2n-2n−2 级爬 2 步上来。
- 状态转移方程: DP[n]=DP[n−1]+DP[n−2]DP[n] = DP[n-1] + DP[n-2]DP[n]=DP[n−1]+DP[n−2]
- 边界条件: DP[1]=1DP[1]=1DP[1]=1,DP[2]=2DP[2]=2DP[2]=2。
2. 进阶优化方向
进阶优化通常指向以下两个方向:
- 方法一(复杂度优化): O(n)O(n)O(n) 时间复杂度,但空间复杂度优化到 O(1)O(1)O(1)。
- 方法二(泛化问题): 每次可以爬 kkk 级台阶,即步长范围是 1,2,…,k1, 2, \dots, k1,2,…,k。
🚀 进阶优化一:空间复杂度优化到 O(1)O(1)O(1)
🎯 目标与原理
- 目标: 将标准爬楼梯问题的时间复杂度保持 O(n)O(n)O(n),空间复杂度优化到 O(1)O(1)O(1)。
- 原理: 由于 DP[n]DP[n]DP[n] 只依赖于前两个状态 DP[n−1]DP[n-1]DP[n−1] 和 DP[n−2]DP[n-2]DP[n−2],我们不需要存储整个 DPDPDP 数组,只需要用三个变量(或两个变量)来滚动更新状态。这被称为滚动数组优化。
💻 Python 代码实现
class Solution:
def climbStairs_O1(self, n: int) -> int:
"""
爬楼梯问题,时间复杂度 O(n),空间复杂度 O(1)。
"""
if n == 1:
return 1
# 定义两个变量存储前两步的状态:
# one_step_before 相当于 DP[i-1]
# two_steps_before 相当于 DP[i-2]
two_steps_before = 1 # 对应 DP[1] = 1
one_step_before = 2 # 对应 DP[2] = 2
# 从 n=3 开始迭代计算
for _ in range(3, n + 1):
# current 相当于 DP[i] = DP[i-1] + DP[i-2]
current = one_step_before + two_steps_before
# 滚动更新状态,为下一次迭代做准备:
# 原来的 DP[i-1] 变成了新的 DP[i-2]
two_steps_before = one_step_before
# 原来的 DP[i] 变成了新的 DP[i-1]
one_step_before = current
return one_step_before # n 级台阶的方法数
# 复杂度分析:
# 时间复杂度:O(n),循环 n-2 次。
# 空间复杂度:O(1),只使用了常数个变量。
🚀 进阶优化二:泛化问题(步长范围 1…k1 \dots k1…k)
🎯 目标与分析
- 问题: 每次可以爬 1,2,…,k1, 2, \dots, k1,2,…,k 级台阶,问:有多少种不同的方法可以爬到 nnn 级楼顶?
- 核心思路: 这是一个变种的动态规划问题。到第 nnn 级的方法数,是从前 kkk 级台阶以不同的步长爬上来的方法数之和。
状态转移方程:
DP[n]=DP[n−1]+DP[n−2]+⋯+DP[n−k]DP[n] = DP[n-1] + DP[n-2] + \dots + DP[n-k]DP[n]=DP[n−1]+DP[n−2]+⋯+DP[n−k]
考察的知识点
- 泛化 DP:能够从 DP[n]=DP[n−1]+DP[n−2]DP[n] = DP[n-1] + DP[n-2]DP[n]=DP[n−1]+DP[n−2] 推广到任意 kkk 步长求和。
- 边界处理:需要正确初始化 DP[0]DP[0]DP[0] 到 DP[k−1]DP[k-1]DP[k−1] 的值。
- 滑动窗口优化(选学): 可以利用前缀和(Prefix Sum)或滑动窗口来将内层循环优化,但通常仍是 O(n⋅k)O(n \cdot k)O(n⋅k) 或 O(n)O(n)O(n)。
💻 Python 代码实现(O(n⋅k)O(n \cdot k)O(n⋅k) 基础实现)
class Solution:
def climbStairs_k(self, n: int, k: int) -> int:
"""
每次可爬 1 到 k 级台阶,计算到达 n 级的方法数。
时间复杂度:O(n * k)
空间复杂度:O(n)
"""
if n == 0: return 1 # 到第0级只有1种方法(原地不动)
# DP[i] 表示到达第 i 级台阶的方法数
dp = [0] * (n + 1)
dp[0] = 1 # 边界条件:到达第0级的方法数是 1 (作为起点)
for i in range(1, n + 1):
# 遍历所有可能的步长 j (1 到 k)
for j in range(1, k + 1):
# 如果 i - j >= 0,说明可以从 i - j 级一步跳上来
if i - j >= 0:
dp[i] += dp[i - j]
return dp[n]
# 复杂度分析:
# 时间复杂度:O(n * k),外层循环 n 次,内层循环 k 次。
# 空间复杂度:O(n),存储 DP 数组。
📈 进一步优化(O(n)O(n)O(n) 时间复杂度)
当 kkk 很大时,O(n⋅k)O(n \cdot k)O(n⋅k) 可能会超时。我们可以通过观察 DP[n]DP[n]DP[n] 和 DP[n−1]DP[n-1]DP[n−1] 的关系,将内层循环优化掉。
我们有:
- DP[n]=DP[n−1]+DP[n−2]+⋯+DP[n−k]DP[n] = DP[n-1] + DP[n-2] + \dots + DP[n-k]DP[n]=DP[n−1]+DP[n−2]+⋯+DP[n−k]
- DP[n−1]=DP[n−2]+DP[n−3]+⋯+DP[n−k]+DP[n−k−1]DP[n-1] = DP[n-2] + DP[n-3] + \dots + DP[n-k] + DP[n-k-1]DP[n−1]=DP[n−2]+DP[n−3]+⋯+DP[n−k]+DP[n−k−1] (假设 n−1≥kn-1 \ge kn−1≥k)
将 (2) 代入 (1):
DP[n]=2⋅DP[n−1]−DP[n−k−1]DP[n] = 2 \cdot DP[n-1] - DP[n-k-1]DP[n]=2⋅DP[n−1]−DP[n−k−1]
利用这个递推关系,可以将时间复杂度降为 O(n)O(n)O(n)。
class Solution:
def climbStairs_k_optimized(self, n: int, k: int) -> int:
"""
O(n) 时间复杂度的优化解法。
"""
if n == 0: return 1
# DP[i] 表示到达第 i 级台阶的方法数
dp = [0] * (n + 1)
dp[0] = 1
# 使用 O(n * k) 的方式计算前 k 步(也可以用 O(n) 的方式)
for i in range(1, n + 1):
# 计算 DP[i] 的累加和
if i <= k:
# 当 i <= k 时,DP[i] = DP[i-1] + ... + DP[0]
dp[i] = sum(dp[:i]) + dp[0] # i=1时 dp[1]=dp[0]+dp[0]=2, 应该是1,修正如下:
dp[i] = sum(dp[0:i])
if i == 1: dp[i] = 1 # 修正边界,dp[0] 应该只有在作为求和项出现,不能多次算入
if i > 1: dp[i] = sum(dp[0:i])
else:
# O(1) 转移:DP[i] = 2 * DP[i-1] - DP[i-k-1]
# 这是最关键的优化步骤
dp[i] = 2 * dp[i - 1] - dp[i - k - 1]
return dp[n]
(注:上面的 O(n)O(n)O(n) 优化实现需要更精细的边界处理,尤其在 i≤ki \le ki≤k 的情况。在实际编程中,如果 kkk 不是很大,直接使用 O(n⋅k)O(n \cdot k)O(n⋅k) 基础实现代码会更简洁和安全。)
核心记忆点:
| 进阶方向 | 复杂度 | 核心思路 | 记忆关键 |
|---|---|---|---|
| 标准问题 | O(n)O(n)O(n), O(1)O(1)O(1) | 斐波那契数列,滚动数组 | prev = current + prev_prev |
| 泛化问题 (1…k1 \dots k1…k) | O(n⋅k)O(n \cdot k)O(n⋅k) | DP[n]=∑j=1kDP[n−j]DP[n] = \sum_{j=1}^k DP[n-j]DP[n]=∑j=1kDP[n−j] | K 步求和,内层循环 K 次 |
| 泛化问题优化 | O(n)O(n)O(n) | DP[n]=2⋅DP[n−1]−DP[n−k−1]DP[n] = 2 \cdot DP[n-1] - DP[n-k-1]DP[n]=2⋅DP[n−1]−DP[n−k−1] | 滑动窗口差分(通过相减消去大部分项) |
更多推荐

所有评论(0)