Python实现可变步长爬楼梯:每次走1至m阶的通用解法

许南星
许南星
发布时间:2026-11-12 | 4人浏览过
原创

深入解析使用Python解决每次可走1至m阶的可变步长爬楼梯问题,提供标准动态规划递推、滑动窗口时间优化与空间压缩的完整通用解法与代码示例。

Python实现可变步长爬楼梯:每次走1至m阶的通用解法
图 0:可变步长爬楼梯模型概览(单次允许跨越 1 至 m 阶)

在经典的爬楼梯问题中,通常限制每次只能跨 1 阶或 2 阶。当规则拓展为单次允许跨越 1 至 m 阶(m ≥ 1)时,目标是计算登上第 n 阶台阶的所有不同走法总数。本文将介绍该问题的通用递推关系,给出标准动态规划实现,并通过维护滑动窗口前缀和将时间复杂度优化至 O(n),同时系统梳理初始边界与极端条件。

一、问题定义与状态转移关系

假设台阶总级数为 n(非负整数),每次单步跨越的阶数可以是集合 {1, 2, ..., m} 中的任意整数。我们需要求出从地面(第 0 阶)到达第 n 阶的合法走法序列总数。

利用动态规划思想,我们定义状态 dp[i] 为到达第 i 阶台阶的方法总数。考虑到达第 i 阶的“最后一步”:

  • 最后一步跨 1 阶:前一个位置只能是第 i-1 阶,方案数为 dp[i-1];
  • 最后一步跨 2 阶:前一个位置只能是第 i-2 阶,方案数为 dp[i-2];
  • ……
  • 最后一步跨 k 阶(1 ≤ k ≤ m 且 k ≤ i):前一个位置是第 i-k 阶,方案数为 dp[i-k]。

因为这些最后跨步的步长互不相同,所形成的走法集合互斥且完备,根据加法原理,到达第 i 阶的总方案数即为最近 m 个可达阶数方案数的总和:

dp[i] = dp[i-1] + dp[i-2] + ... + dp[i-m]   (其中所有下标小于 0 的项取值为 0)

初始状态设定为 dp[0] = 1。这代表站在地面的基本状态方案数为 1,也意味着如果一次性直接跨 k 阶到达第 k 阶,它将继承 dp[0] 的这 1 种直接跨入方案。

Python实现可变步长爬楼梯:每次走1至m阶的通用解法
图 1:状态转移方程推导图(dp[i] 依赖其前方最近 m 个历史状态的累加)

二、通用动态规划代码实现与逐步计算

按照上述状态转移方程,最直接的算法是使用一个长度为 n + 1 的列表记录每个台阶的方案数。外层循环遍历台阶 i(从 1 到 n),内层循环遍历允许的步长 step(从 1 到 m)。

1. 完整实现代码

def climb_stairs_general(n: int, m: int) -> int:
    """
    计算每次最多走 m 阶时,爬到第 n 阶的走法总数。
    
    参数:
        n: 目标楼梯阶数 (n >= 0)
        m: 单次最大允许跨越的阶数 (m >= 1)
    返回:
        走法总数
    """
    # 边界防御
    if n < 0 or m < 1:
        return 0
    if n == 0:
        return 1

    # 初始化 dp 数组,dp[i] 表示登上第 i 阶的方法数
    dp = [0] * (n + 1)
    dp[0] = 1  # 站在地面的初始方案数为 1

    # 递推填表
    for i in range(1, n + 1):
        # 单次跨步最多为 m,但不能超过当前已有的台阶数 i
        max_step = min(i, m)
        for step in range(1, max_step + 1):
            dp[i] += dp[i - step]

    return dp[n]


# 示例验证:n = 5, m = 3
if __name__ == "__main__":
    test_n, test_m = 5, 3
    result = climb_stairs_general(test_n, test_m)
    print(f"台阶数 n = {test_n}, 最大步长 m = {test_m} 时,总走法为: {result}")

2. 执行结果输出示例

# 运行输出:
台阶数 n = 5, 最大步长 m = 3 时,总走法为: 13

为了清晰观察状态值的演变过程,我们可以跟踪 n = 5, m = 3 时的计算步骤:

  • i = 0: 初始化 dp[0] = 1;
  • i = 1: step ∈ [1, min(1, 3)] → dp[1] = dp[0] = 1;
  • i = 2: step ∈ [1, min(2, 3)] → dp[2] = dp[1] + dp[0] = 1 + 1 = 2;
  • i = 3: step ∈ [1, min(3, 3)] → dp[3] = dp[2] + dp[1] + dp[0] = 2 + 1 + 1 = 4;
  • i = 4: step ∈ [1, 3] → dp[4] = dp[3] + dp[2] + dp[1] = 4 + 2 + 1 = 7;
  • i = 5: step ∈ [1, 3] → dp[5] = dp[4] + dp[3] + dp[2] = 7 + 4 + 2 = 13。
Python实现可变步长爬楼梯:每次走1至m阶的通用解法
图 2:逐步填表执行演练流程(展示从基底 dp[0]=1 逐步填满至 dp[5]=13 的过程)

三、关键对比:双重循环累加 vs 滑动窗口维护

为了深入理解算法效率,我们可以对比两种不同的状态更新机制。在相同的输入条件(台阶数 n、最大步长 m)下,实现细节的差异将直接决定时间复杂度是依赖于 m 还是与 m 无关。

对比维度 标准动态规划(双重循环累加) 滑动窗口优化动态规划(维护窗口和)
计算机制 对每个台阶 i,显式循环 1 ~ min(i, m),逐项求和。 维护一个代表最近 m 个台阶和的变量 window_sum,每前进一步仅需一次加法与一次减法。
时间复杂度 O(n × m) O(n),与最大步长 m 的大小完全无关。
空间复杂度 O(n)(亦可压缩至 O(m)) O(n)(亦可压缩至 O(m))
实现复杂度 逻辑直观,不易出错,容易进行边界防护。 需要准确处理滑动窗口的“移入新项”与“移出过期项”时机。
适用场景 m 较小(如 m ≤ 3)时性能差异不明显。 当 m 较大(例如 m = 1000, n = 100000)时,具有绝对性能优势。

为什么这种差异至关重要?观察连续两个状态的表达式:

dp[i]   = dp[i-1] + dp[i-2] + ... + dp[i-m]
dp[i-1] = dp[i-2] + dp[i-3] + ... + dp[i-m-1]

将两式相减可得:dp[i] - dp[i-1] = dp[i-1] - dp[i-m-1]。整理后得到:

dp[i] = 2 * dp[i-1] - dp[i-m-1]

这意味着求 dp[i] 并不需要重复把 m 个数加一遍,只需在前一项的基础上加上新进窗口的 dp[i-1],并减去滑出窗口的 dp[i-m-1]。从 O(m) 的求和开销骤降为 O(1) 的更新。

Python实现可变步长爬楼梯:每次走1至m阶的通用解法
图 3:双重循环累加与滑动窗口维护的核心机制与复杂度对照

四、滑动窗口优化实现与空间压缩

基于滑动窗口机制,我们可以维护一个全局变量 window_sum,其初始值为 0。当计算到 dp[i] 时,执行以下两步:

  1. 将刚跨过的上一阶 dp[i-1] 加入窗口:window_sum += dp[i-1];
  2. 如果跨度超出 m(即 i - 1 - m ≥ 0),将已经超出步长范围的 dp[i - 1 - m] 移出窗口:window_sum -= dp[i - 1 - m];
  3. 此时 window_sum 精确等于前 m 项之和,直接赋值 dp[i] = window_sum。

1. 时间优化后的通用代码

def climb_stairs_sliding_window(n: int, m: int) -> int:
    """
    基于滑动窗口维护最近 m 项和,实现 O(n) 时间复杂度的通用爬楼梯算法。
    """
    if n < 0 or m < 1:
        return 0
    if n == 0:
        return 1

    dp = [0] * (n + 1)
    dp[0] = 1

    window_sum = 0
    for i in range(1, n + 1):
        # 1. 纳入最新前驱 dp[i-1]
        window_sum += dp[i - 1]
        
        # 2. 移除超出最大跨度 m 的过期历史项 dp[i - 1 - m]
        if i - 1 - m >= 0:
            window_sum -= dp[i - 1 - m]
            
        # 3. 记录当前台阶方案数
        dp[i] = window_sum

    return dp[n]

2. 空间压缩优化:将空间从 O(n) 降低至 O(m)

观察状态转移可知,我们自始至终只需要保留最近的 m 个历史状态值。如果 n 极大而内存受限,可以使用一个长度为 m 的双端队列(或循环数组)来存储这些状态,使空间复杂度从 O(n) 优化至 O(m):

from collections import deque

def climb_stairs_space_optimized(n: int, m: int) -> int:
    """
    空间复杂度压缩至 O(m) 的滑动窗口解法。
    """
    if n < 0 or m < 1:
        return 0
    if n == 0:
        return 1

    # 使用固定容量为 m 的双端队列维护最近 m 项
    queue = deque([1])  # 对应 dp[0]
    window_sum = 1      # 当前队列中的总和

    for i in range(1, n + 1):
        # 当前台阶方案数就是队列内元素之和
        current = window_sum
        
        # 准备为下一个台阶服务:维护窗口
        if len(queue) == m:
            removed = queue.popleft()
            window_sum -= removed
            
        queue.append(current)
        window_sum += current

    # 循环结束时,最近入队的值即为 dp[n]
    return queue[-1]

五、常见边界陷阱与测试验证

在实际工程或面试中,容易因为边界情况导致数组越界或计算结果错误。以下是几个容易被忽略的关键细节:

注意事项 1:当 m ≥ n 时的特殊性质
如果允许单次跨越的最大步长 m 大于或等于总阶数 n,那么在从第 0 阶到第 n 阶之间的任意中间阶梯,都可以选择“落脚”或“不落脚”。共有 n - 1 个中间台阶,每个台阶有选或不选 2 种状态,因此总走法恒等于 2^(n-1)。读者可以利用此性质对算法的输出进行快速自测。
注意事项 2:m = 1 时的退化
当 m = 1 时,每次只能走 1 阶,无论 n 多大,走法永远只有唯一定义的一种(1+1+...+1)。算法应能自然返回 1。

综合测试用例与验证

def run_tests():
    test_cases = [
        # (n, m, 预期结果, 说明)
        (0, 3, 1, "0 阶台阶,起点方案数为 1"),
        (1, 3, 1, "1 阶台阶,只能走 1 步"),
        (2, 2, 2, "经典斐波那契走法: [1+1, 2]"),
        (3, 2, 3, "经典斐波那契走法: [1+1+1, 1+2, 2+1]"),
        (4, 4, 8, "m >= n 时,结果为 2^(4-1) = 8"),
        (5, 3, 13, "正文演算实例"),
        (10, 1, 1, "m=1 时退化为唯一走法"),
    ]

    for n, m, expected, desc in test_cases:
        res_dp = climb_stairs_general(n, m)
        res_win = climb_stairs_sliding_window(n, m)
        res_opt = climb_stairs_space_optimized(n, m)
        assert res_dp == expected == res_win == res_opt, f"测试失败: n={n}, m={m}"
        print(f"[通过] n={n:2d}, m={m:2d} -> 结果: {res_dp:2d} ({desc})")

if __name__ == "__main__":
    run_tests()

执行上述测试套件,输出结果如下(示例):

[通过] n= 0, m= 3 -> 结果:  1 (0 阶台阶,起点方案数为 1)
[通过] n= 1, m= 3 -> 结果:  1 (1 阶台阶,只能走 1 步)
[通过] n= 2, m= 2 -> 结果:  2 (经典斐波那契走法: [1+1, 2])
[通过] n= 3, m= 2 -> 结果:  3 (经典斐波那契走法: [1+1+1, 1+2, 2+1])
[通过] n= 4, m= 4 -> 结果:  8 (m >= n 时,结果为 2^(4-1) = 8)
[通过] n= 5, m= 3 -> 结果: 13 (正文演算实例)
[通过] n=10, m= 1 -> 结果:  1 (m=1 时退化为唯一走法)

六、算法选型决策与知识总结

针对不同规模的 n 与 m,我们可以总结出清晰的算法选型策略:

Python实现可变步长爬楼梯:每次走1至m阶的通用解法
图 4:通用解法特性矩阵与选型推荐总结

综上所述,解决“每次走 1 至 m 阶”的通用爬楼梯问题,核心在于建立 dp[i] = ∑ dp[i-step] 的递推思维。理解从标准 O(n × m) 循环累加到 O(n) 滑动窗口维护前缀和的推导过程,能够帮助开发者在面对高并发、大数据量的状态转移计算时,写出兼具正确性与性能的通用 Python 解法。

相关专题:

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表