什么是上楼梯问题?
简单地说…
想象一下有N个台阶的楼梯。如果你每次可以走1步、2步或3步,你有多少种不同的方式可以到达楼梯的顶部?
有一些明显的情况。你可以一次走1步,共走N次。如果N可以被2整除,你可以一次走2步,共走N/2次。同样,如果N可以被3整除,你可以一次走3步,共走N/3次。
然后就是中间的情况。🤔
好消息是,使用每个人都喜欢的递归算法,有一种非常简单的方法来解决这个问题。😨
递归
这是用5行JavaScript代码解决这个问题的方法…
// N是楼梯中的总步数
// stepsTaken是每种组合中已经走过的步数的计数器
function steps(N, stepsTaken = 0) {
if (stepsTaken === N) return 1;
else if (stepsTaken > N) return 0;
return steps(N,stepsTaken + 1) + steps(N,stepsTaken + 2) + steps(N,stepsTaken + 3);
}
顿悟
让我们来解释一下这个解决方法
function steps(N, stepsTaken = 0)
只是一个简单的递归计数器。
我们在楼梯的底部,还没有走过任何步骤。所以stepsTaken = 0。
你面前有3种可能性:走一步,跳上2步,或者跳上3步。
现在我们需要考虑所有3种可能性。所以想象一下你克隆了楼梯和自己2次。此外,你们每个人都有自己携带的stepsTaken变量的版本。你和你的克隆体每个人都会通过这些“门”之一(注意每个人都必须通过不同的门):
steps(N,stepsTaken + 1)
steps(N,stepsTaken + 2)
steps(N,stepsTaken + 3)
一旦你通过你选择的门,你个人的stepsTaken计数器将增加1、2或3(取决于你选择的门)。然后在那之后,你会立即看到另外3扇门:
steps(N,stepsTaken + 1)
steps(N,stepsTaken + 2)
steps(N,stepsTaken + 3)
同样,你会再次克隆自己2次,每个人都会通过这些步骤之一。你的stepsTaken计数器将再次增加1、2或3。这将一直发生,直到:
if (stepsTaken === N) return 1;
else if (stepsTaken > N) return 0;
如果你超过了stepsTaken > N,你的步骤组合不计入到上楼梯的独特方式总数中。
然而,如果(stepsTaken === N),那么 bingo 🤩 你找到了一种合法的上楼梯步骤组合,你返回1以增加上楼梯的方式的计数。
现在记住,我们计算达到N级楼梯顶部的可能步骤组合数量的方法是简单地将所有可能性相加:
return steps(N,stepsTaken + 1) + steps(N,stepsTaken + 2) + steps(N,stepsTaken + 3);
记住,每个合法的步骤组合,如果(stepsTaken === N) return 1,每个不合法的情况(超过步数)则返回0:else if (stepsTaken > N) return 0。
提升
有些人可能会问:很好,这很优雅,但从时间复杂度来看,它不是非常低效吗?
是的,它的时间复杂度非常高。然而,有一个技巧可以显著提高它的效率。
function stairSteps(N) {
const memo = [];
function stepsM(N) {
if (N === 0) return 1;
else if (N < 0) return 0;
if (memo[N] !== undefined) return memo[N];
else {
memo[N] = stepsM(N - 1) + stepsM(N - 2) + stepsM(N - 3);
return memo[N];
}
}
return stepsM(N);
}
解释
代码中定义了一个memo数组,用于存储已经计算过的楼梯阶数的结果。
递归函数stepsM首先判断当前楼梯阶数N的情况:
- 如果N等于0,表示已经爬到了楼梯顶部,返回1,表示找到了一种爬楼梯的方式。
- 如果N小于0,表示当前的爬楼梯方式不可行,返回0。
- 如果memo[N]已经有值,说明之前已经计算过该楼梯阶数的结果,直接返回memo[N]。
- 否则,将stepsM(N - 1) + stepsM(N - 2) + stepsM(N - 3)的结果赋值给memo[N],并返回memo[N]。
最后,返回调用stepsM函数的结果。
这个递归函数的原理是通过将问题分解为子问题来计算爬楼梯的方式数量。每次计算某个楼梯阶数N的方式数量时,先检查是否已经计算过该阶数的结果,如果有则直接返回,避免重复计算。如果没有,则通过递归调用自身,计算N-1、N-2、N-3阶楼梯的方式数量,并将结果保存到memo数组中,以便下次使用。这样可以避免重复计算,提高效率。