#12 爬楼梯的方法

167 阅读2分钟

题目

你面前有一段 N 个台阶的楼梯,你每次可以走 1 级或 2 级台阶。请写一个函数,给定 N 作为输入,输出有多少种不同的爬楼梯方案(走台阶的顺序是相关的)。

比如,如果 N 是 4,则应该输出 5,方案分别为:

  • 1,1,1,1
  • 2,1,1
  • 1,2,1
  • 1,1,2
  • 2,2

现在如果你可以走的台阶数是一个正整数集 X,比如 X = {1,3,5} 表示你每次可以走 1 级、3 级或 5 级台阶,那应该怎么实现这个函数?请把你的函数泛化为可以接收 X 作为参数的实现。

waiting.png

答案

我们先来人肉解答几个台阶数较小的情况,看看能不能找到一些规律:

  • N = 1: [1]
  • N = 2: [1,1], [2]
  • N = 3: [1,2], [1,1,1], [2,1]

有发现什么关联吗?

我能走到第 3 级台阶的方法只有两种:1)先走到第 2 级台阶,然后走 1 级;2)先走到第 1 级台阶,然后走 2 级。用公式表达的话就是:f(3) = f(2) + f(1)。按这个推论,我们来看 N = 4 的时候是否试用?答案是肯定的,你依旧只有两种方法,先走到第 3 级然后走 1 级,或者先走到第 2 级然后走 2 级,所以 f(4) = f(3) + f(2) = 5。让我们泛化一下:

f(n)=f(n1)+f(n2)f(n) = f(n-1) + f(n-2)

这不就是 斐波那契数列 吗!

func staircase(_ n: Int) -> Int {
    // guard 是 swift 语言的关键词,
    // 表示提前检查一个你预期的情况,如果不符合则返回需要的结果
    guard n > 1 else {
        return 1
    }
    return staircase(n - 1) + staircase(n - 2)
}

当然上面的实现方案是非常低效的,有很多重复的计算,时间复杂度是 O(2N),我们可以改成迭代式计算:

func staircase(_ n: Int) -> Int {
    // 当 n = 1 和 2 时的结果值
    var outs = (1, 2)
    for _ in 1..<n {
        outs = (outs.1, outs.0 + outs.1)
    }
    return outs.0
}

现在我们来看一下当可以走的台阶数是一个给定正整数集的情况,以 X = {1,3,5} 为例,我们可以类推出对应的公式和代码:

f(n)=f(n1)+f(n3)+f(n5)f(n) = f(n-1) + f(n-3) + f(n-5)
func staircase(_ n: Int, _ x: Set<Int>) -> Int {
    if n < 0 {
        return 0
    }
    if n == 0 {
        return 1
    }
    // .reduce(0, +) 表示把前面的数组求和
    return (x.map { staircase(n - $0, x) }).reduce(0, +)
}

当前递归调用的时间复杂度依旧很高,我们来优化一下:

func staircase(_ n: Int, _ x: Set<Int>) -> Int {
    var cache = [Int](repeating: 0, count: n + 1)
    cache[0] = 1
    for i in 1...n {
        cache[i] += (x.map { (i < $0) ? 0 : cache[i - $0]}).reduce(0, +)
    }
    return cache[n]
}

cache[i] 里存了走完 i 级台阶可能的方案数量,cache[0] = 1是初始值,这样整个方法的时间复杂度就变成了 O(N)。


题目摘录自 www.dailycodingproblem.com/