问题
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
注意:给定 n 是一个正整数。
示例 1:
输入: 2
输出: 2
解释: 有两种方法可以爬到楼顶。
- 1 阶 + 1 阶
- 2 阶
示例 2:
输入: 3
输出: 3
解释: 有三种方法可以爬到楼顶。
- 1 阶 + 1 阶 + 1 阶
- 1 阶 + 2 阶
- 2 阶 + 1 阶
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/climbing-stairs
解法
一、递归法
题目分析:
- 如果从第0级台阶爬到第1级台阶:有1种方法(爬1个台阶)
- 如果从第0级台阶爬到第2级台阶:有2种方法(爬1个台阶 或 爬2个台阶)
- 如果从第0级台阶爬到第3级台阶:有3种方法
- 先从第0级台阶爬到第1级台阶,再从第1级台阶爬到2级台阶,再从第2级台阶爬到第3级台阶,即1,1,1
- 先从第0级爬1个台阶到第1级台阶,再从第1级爬2个台阶到第3级,即1,2
- 先从第0级爬2个台阶到第2级台阶,再从第2级爬1个台阶到第3级,即2,1
- 如果从第0台阶爬到第4级台阶:有5种方法
- 1,1,1,1
- 1,1,2
- 1,2,1
- 2,1,1
- 2,2
- 如果从第0台阶爬到第5级台阶,有8种方法(这里懒得列了,你可以试试)
转换成数学的逻辑题,很容易看得出来:
f(1)=1
f(2)=2
f(3)=3
f(4)=5
f(5)=8
f(6)=13
f(7)=21
即1,2,3,5,8,13,21
可以发现,除了第一个数和第二个数外,从第三个数开始,他前面两个数的和就是他自身。
相当于
-
第三个数(3)= 第一个数(1)+ 第二个数(2)
-
第四个数(5)= 第二个数(2)+ 第三个数(3)
-
第五个数(8)= 第三个数(3)+ 第四个数(5)
可得出:第n个数 = 第(n-2)个数 + 第(n-1)个数
写成递推公式则是:f(n) = f(n-2)+f(n-1)
这样即可使用递归来解决。
代码如下:
class Solution
{
/**
* @param Integer $n
* @return Integer
*/
function climbStairs($n)
{
//如果是爬到第1级台阶,直接返回1
//如果是爬到第2级台阶,直接返回2
if ($n === 1 || $n === 2) {
return $n;
}
//如果是爬到>2级的台阶,则按照递推公式,使用递归返回
return $this->climbStairs($n - 2) + $this->climbStairs($n - 1);
}
}
提交一下:
这就很尴尬了,无疑,使用递归是能够实现的,但是时间却过长。
优化下递归:
以爬到第5级台阶为例,可以得出以下递归二叉树图
在递归中,计算第5级台阶的方法数时,便需要计算第4级台阶的方法数和第3级台阶的方法数;
而在计算第4级台阶的方法数时,是计算了第3级台阶的方法数和***第2级台阶的方法数***
在计算第3级台阶的方法数是,是计算了第1级台阶的方法数,并重复计算了***第2级台阶的方法数***
当然,同样被重复不止第2级台阶的方法数,哪怕在上面,第3级台阶的方法数和第1级台阶的方法数也是被重复计算了的
重复计算无异于提高了时间复杂度,这里,可考虑使用记忆数组,来记录已经计算过的第n级方法数,避免重复计算,将将时间复杂度优化到O(n)
代码如下:
class Solution
{
//记忆数组,记录已经计算过的结果数据,避免重复计算,减少时间复杂度
private $momeryArr = [];
/**
* @param Integer $n
* @return Integer
*/
function climbStairs($n)
{
//如果已经有记忆了的计算结果,直接返回
if (isset($this->momeryArr[$n])) {
return $this->momeryArr[$n];
}
//如果是爬到第1级台阶,直接返回1
//如果是爬到第2级台阶,直接返回2
if ($n === 1 || $n === 2) {
//将计算结果记录下来
$this->momeryArr[$n] = $n;
return $n;
}
//如果是爬到>2级的台阶,则按照递推公式,使用递归返回
//将计算结果记录下来
$this->momeryArr[$n] = $this->climbStairs($n - 2) + $this->climbStairs($n - 1);
return $this->momeryArr[$n];
}
}
这样就提交成功,不超时了
二、动态规划法
按照递归的题解思路,我们已经得到递推公式:
- 当n=1时,f(n) = 1
- 当n=2时,f(n) = 2
- 当n=3时,f(n) = f(n-1) + f(n-2)
代码如下:
class Solution
{
/**
* @param Integer $n
* @return Integer
*/
function climbStairs($n)
{
//如果是爬到第1级台阶,直接返回1
//如果是爬到第2级台阶,直接返回2
if ($n === 1 || $n === 2) {
//将计算结果记录下来
return $n;
}
$climbStairs[1] = 1; //f(1)=1
$climbStairs[2] = 2; //f(2)=2
//所要计算的内容,将计算结果保存到数组$climbStairs中,最终得到f(n)
for ($i = 3; $i <= $n; $i++) {
$climbStairs[$i] = $climbStairs[$i - 1] + $climbStairs[$i - 2]; // f(n) = f(n-1) + f(n-2)
}
//得到最终结果
return $climbStairs[$n];
}
}
使用滚动数组的逻辑来处理:使用三个变量$first
、$second
、$third
,来取代$climbStairs
数组。当n=1和n=2时,按照原逻辑返回即可。
当n=5时,我们先计算n=3的值,再计算n=4的值,再把两者相加
- 先默认 f i r s t = 1 , first=1, first=1,second=2,对应f(1)和f(2)
- 求f(3)时,即是 t h i r d = third= third=first+ s e c o n d , f ( 3 ) 便 是 second,f(3)便是 second,f(3)便是third
- 求f(4)时,我们也要实现f(4)= f i r s t + first+ first+second,这样才能减少空间复杂度。那么, f i r s t 就 应 是 f ( 2 ) , first就应是f(2), first就应是f(2),second就应是f(3)。所以在上一步时,就应该将两者的值给替换成上述情况。
- 回到求f(3)时的情景。在得到 t h i r d 后 , 应 该 将 third后,应该将 third后,应该将second的值给 f i r s t , first, first,third的值给 s e c o n d , 这 样 , second,这样, second,这样,first就一直是f(n-2),$third是f(n-1)
- 最后,求f(5),也是同样的 t h i r d = third= third=first+$second即能得到结果,循环也应到此结束,返回所求的f(5)的值
代码如下:
class Solution
{
/**
* @param Integer $n
* @return Integer
*/
function climbStairs($n)
{
//如果是爬到第1级台阶,直接返回1
//如果是爬到第2级台阶,直接返回2
if ($n === 1 || $n === 2) {
//将计算结果记录下来
return $n;
}
$first = 1; //f(1)=1
$second = 2; //f(2)=2
//所要计算的内容,将计算结果保存到数组$climbStairs中,最终得到f(n)
for ($i = 3; $i <= $n; $i++) {
$third = $first + $second; // f(n) = f(n-1) + f(n-2)
$first = $second; //更新$first的值,让下一次循环时,它是f(n-2)
$second = $third; //更新$second的值,让下一次循环时,它是f(n-1)
}
//得到最终结果
return $third;
}
}
三、矩阵法
首先,来了解温习一下:矩阵的乘法
两个矩阵的乘法仅当第一个矩阵A 的列数和另一个矩阵 B 的行数相等时才能定义。如A是m×n矩阵和B是n×p矩阵,他们的乘积C是一个m×p矩阵
C = ( c i j ) C=(c_{ij}) C=(cij)
它的一个元素:
c i , j = a i , 1 b 1 , j + a i , 2 b 2 , j + ⋯ + a i , n b n , j = ∑ r = 1 n a i , r b r , j c_{i,j}=a_{i,1}b_{1,j}+a_{i,2}b_{2,j}+\cdots+a_{i,n}b_{n,j}=\sum\limits_{r=1}^na_{i,r}b{r,j} ci,j=ai,1b1,j+ai,2b2,j+⋯+ai,nbn,j=r=1∑nai,rbr,j
并将此乘积记为:
C = A B C=AB C=AB
例如:
[ 1 0 2 − 1 3 1 ] × [ 3 1 2 1 1 0 ] = [ ( 1 × 3 + 0 × 2 + 2 × 1 ) ( 1 × 1 + 0 × 1 + 2 × 0 ) ( − 1 × 3 + 3 × 2 + 1 × 1 ) ( − 1 × 1 + 3 × 1 + 1 × 0 ) ] = [ 5 1 4 2 ] \left[ \begin{matrix} 1 & 0 & 2 \\ -1 & 3 & 1 \end{matrix} \right] \times \left[ \begin{matrix} 3 & 1 \\ 2 & 1 \\ 1 & 0 \end{matrix} \right] = \left[ \begin{matrix} (1 \times 3 + 0 \times 2 + 2 \times 1) & ( 1 \times 1 + 0 \times 1 + 2 \times 0 ) \\ (-1 \times 3 + 3 \times 2 + 1 \times 1) & ( -1 \times 1 + 3 \times 1 + 1 \times 0 ) \end{matrix} \right] = \left[ \begin{matrix} 5 & 1 \\ 4 & 2 \end{matrix} \right] [1−10321]×⎣⎡321110⎦⎤=[(1×3+0×2+2×1)(−1×3+3×2+1×1)(1×1+0×1+2×0)(−1×1+3×1+1×0)]=[5412]
矩阵的乘法满足以下运算律:结合律:
( A B ) C = A ( B C ) (AB)C=A(BC) (AB)C=A(BC)
左分配律:
( A + B ) C = A C + B C (A+B)C=AC+BC (A+B)C=AC+BC
右分配律:
C ( A + B ) = C A + C B C(A+B)=CA+CB C(A+B)=CA+CB
矩阵乘法不满足交换律。
从上述解题思路已知:
F
n
+
1
=
F
n
+
F
n
−
1
F_{n+1}=F_n+F_{n-1}
Fn+1=Fn+Fn−1
现在回到题目,首先我们可以构建这样一个递推关系:
[
1
1
1
0
]
×
[
f
(
n
)
f
(
n
−
1
)
]
=
[
f
(
n
)
+
f
(
n
−
1
)
f
(
n
)
]
=
[
f
(
n
+
1
)
f
(
n
)
]
\left[\begin{matrix}1 & 1\\1 & 0\end{matrix}\right]\times\left[\begin{matrix}f(n)\\f(n-1)\end{matrix}\right]=\left[\begin{matrix}f(n)+f(n-1)\\f(n)\end{matrix}\right]=\left[\begin{matrix}f(n+1)\\f(n)\end{matrix}\right]
[1110]×[f(n)f(n−1)]=[f(n)+f(n−1)f(n)]=[f(n+1)f(n)]
因此可得
[
f
(
n
+
1
)
f
(
n
)
]
=
[
1
1
1
0
]
n
×
[
f
(
1
)
f
(
0
)
]
\left[\begin{matrix}f(n+1)\\f(n)\end{matrix}\right]=\left[\begin{matrix}1 & 1\\1 & 0\end{matrix}\right]^n\times\left[\begin{matrix}f(1)\\f(0)\end{matrix}\right]
[f(n+1)f(n)]=[1110]n×[f(1)f(0)]
而f(1)=1,f(0)=0,进行公式转换
[
1
1
1
0
]
n
×
[
1
0
]
=
[
m
00
m
01
m
10
m
11
]
×
[
1
0
]
=
[
m
00
m
10
]
\left[\begin{matrix}1 & 1\\1 & 0\end{matrix}\right]^n\times\left[\begin{matrix}1\\0\end{matrix}\right]=\left[\begin{matrix}m00 & m01\\m10 & m11\end{matrix}\right]\times\left[\begin{matrix}1\\0\end{matrix}\right]=\left[\begin{matrix}m00\\m10\end{matrix}\right]
[1110]n×[10]=[m00m10m01m11]×[10]=[m00m10]
也就是,所求的n位置的方法数(fn),即
[
1
1
1
0
]
n
\left[\begin{matrix}1 & 1 \\1 & 0\end{matrix}\right]^n
[1110]n
获取其矩阵左下角m10
代码如下:
class Solution
{
/**
* @param Integer $n
* @return Integer
*/
function climbStairs($n)
{
$m = [
[1, 1],
[1, 0]
];
//计算$m的n次幂
$res = $this->pow($m, $n);
//返回$m矩阵的左下角元素$m10
return $res[1][0];
}
/**
* @title 计算矩阵$a的n次幂
* @param $a
* @param $n
* @return array|mixed
*/
function pow($a, $n)
{
$ret = [
[0, 1],
[1, 0]
];
//使用二分法获取
while ($n > 0) {
if (($n & 1) == 1) {
$ret = $this->multiply($ret, $a);
}
$n >>= 1;
$a = $this->multiply($a, $a);
}
return $ret;
}
/**
* @title 计算两个两行两列的矩阵的积
* @param $a
* @param $b
* @return mixed
*/
function multiply($a, $b)
{
for ($i = 0; $i < 2; $i++) {
for ($j = 0; $j < 2; $j++) {
$c[$i][$j] = $a[$i][0] * $b[0][$j] + $a[$i][1] * $b[1][$j];
}
}
return $c;
}
}
四、通项公式
之前的方法我们已经讨论了 f(n)f(n) 是齐次线性递推,根据递推方程 f(n) = f(n - 1) + f(n - 2),我们可以写出这样的特征方程:
x
2
=
x
+
1
x^2=x+1
x2=x+1
求得
x
1
=
1
+
5
2
x_1=\frac{1+\sqrt{5}}{2}
x1=21+5
x
2
=
1
−
5
2
x_2=\frac{1-\sqrt{5}}{2}
x2=21−5
设通解为
f
(
n
)
=
c
1
x
1
n
+
c
2
x
2
n
f(n)=c_1x_1^n+c_2x_2^n
f(n)=c1x1n+c2x2n
代入初始条件f(1)=1,f(2)=1,可得
c
1
=
1
5
c_1=\frac{1}{\sqrt{5}}
c1=51
c
2
=
−
1
5
c_2=-\frac{1}{\sqrt{5}}
c2=−51
得到递推数列的通项公式
f
(
n
)
=
1
5
[
(
1
+
5
2
)
n
−
(
1
−
5
2
)
n
]
f(n)=\frac{1}{\sqrt{5}}\left[\left(\frac{1+\sqrt{5}}{2}\right)^n-\left(\frac{1-\sqrt{5}}{2}\right)^n\right]
f(n)=51[(21+5)n−(21−5)n]
接着我们就可以通过这个公式直接求第 n 项了。
代码如下:
class Solution
{
/**
* @param Integer $n
* @return Integer
*/
function climbStairs($n)
{
//套用通用公式进行计算
$sqrt5 = sqrt(5);
$fibn = pow((1 + $sqrt5) / 2, $n + 1) - pow((1 - $sqrt5) / 2, $n + 1);
return (int)($fibn / $sqrt5);
}
}
以上便是“爬楼梯”的各种解法,不得不说,人类的智慧是无穷的,这里搬砖外加自己理解总结,希望能够让你也有所提高。
参考文章:LeetCode-Solution