70. 爬楼梯 (climbing stairs)

3,861 阅读2分钟

"斐波那契,动态规划开始的地方"

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第14天,点击查看活动详情

70. 爬楼梯 题目描述:假设你正在爬楼梯。需要 nn 阶你才能到达楼顶。每次你可以爬 1122 个台阶。你有多少种不同的方法可以爬到楼顶呢?输入 nn = 7,输出 2121

中规中矩的动态规划

动态规划问题最难的一点就是找到问题的 最优子结构,即如何定义 dpdp 状态数组和 dpdp 状态转移方程,而最简单的动态规划问题的最优子结构就写在问题的题面上。

对于 nn 级台阶,如果只知道爬到第 n1n-1 级台阶的不同方法,记做 Sn1S_{n-1},再一次性爬一阶即可到达顶点;如果只知道爬到第 n2n-2 级台阶的不同方法,记做 Sn2S_{n-2},再一次性爬两阶即可到达顶点,所以爬到第 nn 级台阶共有 SnS_n 种方法,Sn=Sn1+Sn2S_n = S_{n-1} + S_{n-2}

1、确定 dp 状态数组

定义 dp[i]dp[i] 为爬上 ii 级台阶的不同方法数,其中 i[1,n]i \in [1, n];

NOTE: ii 的定义域虽为 [1,n][1, n],但是数组是从 00 开始的,所以初始化 dpdp 的长度应该是 n+1n + 1

2、确定 dp 状态方程

dpdp 的状态转移方程:

dp[i]=dp[i1]+dp[i2]dp[i] = dp[i - 1] + dp[i - 2]

3、确定 dp 初始状态

观察状态方法,有 i1i - 1i2i - 2 两项,故我们需要定义,

  • i=1i = 1 时,11 级台阶只能向上爬一步,即 dp[1]=1dp[1] = 1;

  • i=2i = 2 时,22 级台阶可以一步一步爬,也可以一次性爬两步,即 dp[2]=2dp[2] = 2

NOTE: 这里无需纠结 00 级台阶,即 dp[0]=0dp[0] = 0 还是 11 的问题,这本身是一个无意义的点,而且循环遍历时也不会使用这个值,忽略即可。

4、确定遍历顺序

由于 i=1i = 1i=2i = 2 均已定义完毕,故遍历顺序从 i=3i = 3i=ni = n

5、确定最终返回值

回归状态定义,爬上 nn 级台阶的不同方法数为 dp[n]dp[n]

6、代码示例

/**
 * 空间复杂度 O(n)
 * 时间复杂度 O(n)
 */
function climbStairs(n: number): number {
    const dp = new Array(n + 1).fill(0);
    dp[1] = 1;
    dp[2] = 2;

    for (let i = 3; i <=n; i++) {
        dp[i] = dp[i - 1] + dp[i - 2];
    }

    return dp[n];
};

状态压缩,dp[i]dp[i] 仅与 dp[i1]dp[i-1]dp[i2]dp[i-2] 相关,所以可以将空间复杂度 O(n)O(n) 压缩成 O(1)O(1)

/**
 * 空间复杂度 O(1)
 * 时间复杂度 O(n)
 */
function climbStairs(n: number): number {
    if (n <= 2) return n;
    
    let dp1 = 1, dp2 = 2;
   
    for (let i = 3; i <= n; i++) {
        let temp = dp2;
        dp2 = dp1 + dp2;
        dp1 = temp;
    }
    
    return dp2;
};

思维提升-斐波那契通项公式

细想爬楼梯问题,类似于斐波那契数列,而斐波那契数列的通项公式为,dp[n]=15×[(1+52)n(152)n]dp[n] = \frac{1}{\sqrt 5} \times [({\frac{1+{\sqrt 5}}{2}})^n-({\frac{1-{\sqrt 5}}{2}})^n]

  • 斐波那契数列展开:1、1、2、3、5、 8、13、21、34...

  • 爬楼梯的数列展开:1、2、3、5、8、13、21、34、55...

发现规律了吧~~所以对于爬楼梯问题的通项公式为,

dp[n]=15×[(1+52)n+1(152)n+1]dp[n] = \frac{1}{\sqrt 5} \times [({\frac{1+{\sqrt 5}}{2}})^{n+1}-({\frac{1-{\sqrt 5}}{2}})^{n+1}]

代码示例

/**
 * 空间复杂度O(log(n))
 * 时间复杂度O(log(n))
 */
function climbStairs(n: number): number {
    if (n <= 2) {
        return n;
    }

    const sqrt5 = Math.sqrt(5);
    const fibN = Math.pow((1 + sqrt5) / 2, n + 1) - Math.pow((1 - sqrt5) / 2, n + 1);
    return Math.round(fibN / sqrt5);
};

💥 NOTE,JS中的 Math.pow 函数空间复杂度与时间复杂度大致为 log(n)log(n),具体与引擎实现有关,不做深入分析。

参考

# 重识动态规划