收到了,爬楼梯(Climbing Stairs)是动态规划入门的经典问题。您提到“进阶优化”,通常指的是在标准问题的基础上,考虑更复杂的步长限制,或者要求空间复杂度的极致优化

我们先回顾标准问题,然后重点分析两种“进阶优化”的思路。


💡 题目分析:爬楼梯(Climbing Stairs)

1. 标准问题回顾

问题: 假设你正在爬楼梯。楼梯有 nnn 级台阶。每次你可以爬 1 级或者 2 级台阶。问:有多少种不同的方法可以爬到楼顶?

核心思路: 这是一个典型的斐波那契(Fibonacci)数列问题。

  • 到第 nnn 级台阶的方法数 DP[n]DP[n]DP[n]
    • 只能从第 n−1n-1n1 级爬 1 步上来。
    • 或者从第 n−2n-2n2 级爬 2 步上来。
  • 状态转移方程: DP[n]=DP[n−1]+DP[n−2]DP[n] = DP[n-1] + DP[n-2]DP[n]=DP[n1]+DP[n2]
  • 边界条件: DP[1]=1DP[1]=1DP[1]=1DP[2]=2DP[2]=2DP[2]=2

2. 进阶优化方向

进阶优化通常指向以下两个方向:

  1. 方法一(复杂度优化): O(n)O(n)O(n) 时间复杂度,但空间复杂度优化到 O(1)O(1)O(1)
  2. 方法二(泛化问题): 每次可以爬 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[n1]DP[n−2]DP[n-2]DP[n2],我们不需要存储整个 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 k1k

🎯 目标与分析

  • 问题: 每次可以爬 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[n1]+DP[n2]++DP[nk]

考察的知识点
  • 泛化 DP:能够从 DP[n]=DP[n−1]+DP[n−2]DP[n] = DP[n-1] + DP[n-2]DP[n]=DP[n1]+DP[n2] 推广到任意 kkk 步长求和。
  • 边界处理:需要正确初始化 DP[0]DP[0]DP[0]DP[k−1]DP[k-1]DP[k1] 的值。
  • 滑动窗口优化(选学): 可以利用前缀和(Prefix Sum)或滑动窗口来将内层循环优化,但通常仍是 O(n⋅k)O(n \cdot k)O(nk)O(n)O(n)O(n)

💻 Python 代码实现(O(n⋅k)O(n \cdot k)O(nk) 基础实现)

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(nk) 可能会超时。我们可以通过观察 DP[n]DP[n]DP[n]DP[n−1]DP[n-1]DP[n1] 的关系,将内层循环优化掉。

我们有:

  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[n1]+DP[n2]++DP[nk]
  2. 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[n1]=DP[n2]+DP[n3]++DP[nk]+DP[nk1] (假设 n−1≥kn-1 \ge kn1k)

将 (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]=2DP[n1]DP[nk1]

利用这个递推关系,可以将时间复杂度降为 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 kik 的情况。在实际编程中,如果 kkk 不是很大,直接使用 O(n⋅k)O(n \cdot k)O(nk) 基础实现代码会更简洁和安全。)

核心记忆点:

进阶方向 复杂度 核心思路 记忆关键
标准问题 O(n)O(n)O(n), O(1)O(1)O(1) 斐波那契数列,滚动数组 prev = current + prev_prev
泛化问题 (1…k1 \dots k1k) O(n⋅k)O(n \cdot k)O(nk) DP[n]=∑j=1kDP[n−j]DP[n] = \sum_{j=1}^k DP[n-j]DP[n]=j=1kDP[nj] 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]=2DP[n1]DP[nk1] 滑动窗口差分(通过相减消去大部分项)
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐