【最详细的算法视频题解】【动态规划】—— 斐波那契数列、 Leetcode 70. 爬楼梯

斐波那契数列

斐波那契数列(Fibonacci sequence),又称黄金分割数列、因数学家列昂纳多·斐波那契(Leonardoda Fibonacci)以兔子繁殖为例子而引入,故又称为“兔子数列”,指的是这样一个数列:1、1、2、3、5、8、13、21、34、……在数学上,斐波那契数列以如下被以递推的方法定义:F(1)=1,F(2)=1, F(n)=F(n - 1)+F(n - 2)(n ≥ 3,n ∈ N*)。

要点

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


暴力递归代码

时间复杂度: O ( 2 N ) O(2^N) O(2N)
将公式代入递归结构,找到基础的情况。

public int f1(int n) {
	if (n < 1) {//2、1、2、3、5、8、13、21、34 如果传入的n比1小,不用计算。
		return 0;
	}
	if (n <= 2) {//3. base case: n=1,n=2 的时候返回 1
		return 1;
	}
	return f1(n - 1) + f1(n - 2);//1.将公式代入递归结构
}
解释

斐波那契数列图片
我们以图片为例
如果当前传入的 n = 8 n=8 n=8,在递归过程中会分解成如图结构。
这里没有将其它的分支全部画完,细心的朋友应该能发现它们在到达最底部的时候总会分解成如下结构:
base case
那么当到达 f(2)的时候,如果此时已经知道 f(2) = f(1) 、f(0) = 0,那么在返回上层的时候就能够计算出结果。


备忘录

当前理解了暴力递归的过程,再仔细看看图例。
你会发现在递归分解的过程中出现太多重复的递归子结构(同颜色标记)
递归子结构
这里可以看到实在是有太多重复了,并且随着N的增加,这些重复运算呈指数级增长。
所以想办法将这些重复的运算只保留一次,当下次遇到相同的递归子结构的时候直接使用之前运算出来的结果。

HashMap<Integer, Integer> f2Map = new HashMap<>();
public int f2(int n) {
	if (n < 1) {
		return 0;
	}
	if (n <= 2) {
		return 1;
	}
	if (f2Map.containsKey(n)) {//1.如果备忘录已经存在某个n的结果,直接使用就行了。
		return f2Map.get(n);
	}
	int res = f2(n - 1) + f2(n - 2);
	f2Map.put(n, res);//将当前n的结果记录到备忘录。
	return res;
}

时间复杂度: O ( N ) O(N) O(N)
额外空间复杂度: O ( N ) O(N) O(N)


顺序累加

斐波那契数列存在明显的特征,先看一下公式
斐波那契数列
整个的运算过程已经很明显了,只要把这个过程实现。

public int f3(int n) {
	if (n < 1) {
		return 0;
	}
	if (n <= 2) {
		return 1;
	}
	int now = 2;//1.记录第三个位置的结果。
	int pre = 1;//2.记录上一个位置的结果。
	for (int i = 3; i < n; i++) {//3.从第四个位置开始累加。
		int tmp = now; //5.记录计算前的结果,当做新的上一个值
		now = now + pre;//4.当前+之前 = 新的结果
		pre = tmp;//6.修改上一个值。
	}
	return now;
}

时间复杂度: O ( N ) O(N) O(N)
额外空间复杂度: O ( 1 ) O(1) O(1)


大杀器:矩阵运算 O ( l o g N ) O(logN) O(logN)

这个需要具备一点数学功底,当然你也可以边看边学习。
已知公式 F ( n ) = F ( n − 1 ) + f ( n − 2 ) F(n)=F(n-1)+f(n-2) F(n)=F(n1)+f(n2)是一个二阶递推数列

所谓二阶递推数列,就是已知前两项(一般都是),然后给出连续三项的之间的关系,然后让你确定通项公式。

简化推导过程:
[ f ( n ) f ( n − 1 ) ] = [ a b c d ] × [ f ( n − 1 ) f ( n − 2 ) ] (1) { \begin{bmatrix} f(n) \\ f(n-1) \end{bmatrix} }={ \begin{bmatrix} a & b \\ c & d \end{bmatrix} }\times{ \begin{bmatrix} f(n-1) \\ f(n-2) \end{bmatrix} }\tag{1} [f(n)f(n1)]=[acbd]×[f(n1)f(n2)](1)
[ f ( n ) f ( n − 1 ) ] = [ a ∗ f ( n − 1 ) + b ∗ f ( n − 2 ) c ∗ f ( n − 1 ) + d ∗ f ( n − 2 ) ] (2) { \begin{bmatrix} f(n) \\ f(n-1) \end{bmatrix} }={ \begin{bmatrix} a*f(n-1)&+&b*f(n-2) \\ c*f(n-1)&+&d*f(n-2)\end{bmatrix} }\tag{2} [f(n)f(n1)]=[af(n1)cf(n1)++bf(n2)df(n2)](2)
令 a = 1 , b = 1 此 时 f ( n ) = 1 × f ( n − 1 ) + 1 × f ( n − 2 ) (3) 令 a=1,b=1 此时f(n)=1\times f(n-1)+1\times f(n-2)\tag{3} a=1,b=1f(n)=1×f(n1)+1×f(n2)(3)
, 令 c = 1 , d = 0 此 时 f ( n − 1 ) = 1 × f ( n − 1 ) + 0 × f ( n − 2 ) (4) ,令 c=1,d=0 此时f(n-1)=1\times f(n-1)+0\times f(n-2)\tag{4} c=1,d=0f(n1)=1×f(n1)+0×f(n2)(4)
[ a b c d ] = [ 1 1 1 0 ] (5) { \begin{bmatrix} a & b \\ c & d \end{bmatrix} } = { \begin{bmatrix} 1 & 1 \\ 1 & 0 \end{bmatrix} }\tag{5} [acbd]=[1110](5)
[ f ( 3 ) f ( 2 ) ] = [ 1 1 1 0 ] × [ f ( 2 ) f ( 1 ) ] = [ 1 1 1 0 ] × [ 1 1 ] (6) { \begin{bmatrix} f(3) \\ f(2) \end{bmatrix} }={ \begin{bmatrix} 1 & 1 \\ 1 & 0 \end{bmatrix} }\times{ \begin{bmatrix} f(2) \\ f(1) \end{bmatrix} }={ \begin{bmatrix} 1 & 1 \\ 1 & 0 \end{bmatrix} }\times{ \begin{bmatrix} 1 \\ 1 \end{bmatrix} }\tag{6} [f(3)f(2)]=[1110]×[f(2)f(1)]=[1110]×[11](6)
[ f ( 4 ) f ( 3 ) ] = [ 1 1 1 0 ] × [ f ( 3 ) f ( 2 ) ] = [ 1 1 1 0 ] 2 × [ 1 1 ] (7) { \begin{bmatrix} f(4) \\ f(3) \end{bmatrix} }={ \begin{bmatrix} 1 & 1 \\ 1 & 0 \end{bmatrix} }\times{ \begin{bmatrix} f(3) \\ f(2) \end{bmatrix} }={ \begin{bmatrix} 1 & 1 \\ 1 & 0 \end{bmatrix} }^2\times { \begin{bmatrix} 1 \\ 1 \end{bmatrix} }\tag{7} [f(4)f(3)]=[1110]×[f(3)f(2)]=[1110]2×[11](7)
[ f ( n ) f ( n − 1 ) ] = [ 1 1 1 0 ] n − 2 × [ 1 1 ] (8) { \begin{bmatrix} f(n) \\ f(n-1) \end{bmatrix} }={ \begin{bmatrix} 1 & 1 \\ 1 & 0 \end{bmatrix} }^{n-2}\times{ \begin{bmatrix} 1 \\ 1 \end{bmatrix} }\tag{8} [f(n)f(n1)]=[1110]n2×[11](8)
那么可以看到求第N项的问题就变成了求矩阵的N次方问题,求一个整数的N次方就可以再 O ( l o g N ) O(logN) O(logN)的时间内解决问题。
  假设一个整数是10,如何快速求解10的75次方。
  1. 75的二进制数形式1001011
  2. 10的75次方 = 1 0 64 × 1 0 8 × 1 0 2 × 1 0 1 10^{64}\times10^{8}\times10^{2}\times10^{1} 1064×108×102×101
  在这个过程中先求出 1 0 1 10^{1} 101,然后求出 1 0 2 10^{2} 102 1 0 4 10^{4} 104
  根据二进制位上是1的话将这些结果相乘。

代码中 一、二、三… 代表阅读顺序。

public int f4(int n) {
	if (n < 1) {
		return 0;
	}
	if (n == 1 || n == 2) {
		return 1;
	}
	int[][] base = {{1, 1}, {1, 0}};//1.推到出来的基础情况矩阵
	int[][] res = matrixPower(base, n - 2);//2.计算矩阵的n-2次方,n<=2的时候已经直接返回了。
	return res[0][0] + res[0][1];// 3. 相当于矩阵 ×(1,1)
}

public static int[][] matrixPower(int[][] m, int p) {
	if (m == null || m.length == 0) {//0.直接返回
		return null;
	}
	int[][] res = new int[m.length][m[0].length];//1.创建一个单位矩阵,与某个矩阵x相乘之后保证x保持不变。
	for (int i = 0; i < res.length; i++) {
		res[i][i] = 1;
	}
	int[][] tmp = m;
	for (; p != 0; p >>= 1) {//2.每次都将p右移一个位置,比如二进制 1010>>101>10>1>0 即 10>5>2>1>0
		if ((p & 1) != 0) {//3.判断是不是奇数,奇数的情况特殊,没办法直接做矩阵平方的运算,将它乘以单位矩阵保存下来。
			res = muliMatrix(res, tmp);//5.比如在5的时候 tmp * tmp^4,如果不将左侧的tmp记录下来那么结果就错了。
		}
		tmp = muliMatrix(tmp, tmp);//4.矩阵平方。
	}
	return res;
}

public static int[][] muliMatrix(int[][] m1, int[][] m2) {
	//1.矩阵乘法,要求m1的行等于m2的列。
	if (m1 == null || m2 == null || m1.length == 0 || m2.length == 0 || m1.length != m2[0].length) {
		return null;
	}
	int col = m1.length;//2.结果矩阵的行数等于m1的行数。
	int row = m2[0].length;//3.结果矩阵的列数等于m2的列数。
	int[][] res = new int[col][row];//4.根据行列创建一个结果矩阵
	//5.将计算结果存入结果矩阵,用两层for循环的遍历结果矩阵的每一个格子
	for (int i = 0; i < col; i++) {
		for (int j = 0; j < row; j++) {
			for (int k = 0; k < m2.length; k++) {//将m1这一行的数分别乘以m2这一列的数,其中m2.length控制相层的个数。
				res[i][j] += m1[i][k] * m2[k][j];
			}
		}
	}
	return res;
}

Leetcode 70. 爬楼梯

原题链接
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
注意:给定 n 是一个正整数。
示例 1:

输入: 2
输出: 2
解释: 有两种方法可以爬到楼顶。
1.  1 阶 + 1 阶
2.  2 阶

示例 2:

输入: 3
输出: 3
解释: 有三种方法可以爬到楼顶。
4.  1 阶 + 1 阶 + 1 阶
5.  1 阶 + 2 阶
6.  2 阶 + 1 阶

要点

设结果为 x
n = 1 n=1 n=1 的时候, x = 1 x=1 x=1
n = 2 n=2 n=2 的时候, x = 2 x=2 x=2
n = 3 n=3 n=3 的时候, x = 3 x=3 x=3
n = 4 n=4 n=4 的时候, x = 5 x=5 x=5
这个过程与斐波那契数列是一致的,但是需要注意这里是1,2,3,5不是1,1,2,3,5
那么只要将n的值往上加1n = n+1,此时结果与斐波那契数列一致。

解法

f ( n ) = f ( n + 1 ) f(n) = f(n+1) f(n)=f(n+1)
将题目给的n加上1,调用斐波那契数列的解题方法就能得出结果。

代码
public int f4(int n) {
	n+=1;//加1
	if (n < 1) {
		return 0;
	}
	if (n == 1 || n == 2) {
		return 1;
	}
	int[][] base = {{1, 1}, {1, 0}};//1.推到出来的基础情况矩阵
	int[][] res = matrixPower(base, n - 2);//2.计算矩阵的n-2次方,n<=2的时候已经直接返回了。
	return res[0][0] + res[0][1];// 3. 相当于矩阵 ×(1,1)
}
解释

这里可以把矩阵赋值int[][] tmp = m;去掉,直接使用m对提交的性能提升。


结尾

如果递归式子严格符合 F ( n ) = a × F ( n − 1 ) + b × F ( n − 2 ) + . . . + k × F ( n − i ) F(n)=a\times F(n-1)+b\times F(n-2)+...+k\times F(n-i) F(n)=a×F(n1)+b×F(n2)+...+k×F(ni),那么它就是一个 i i i阶的递推式。一律可以用矩阵乘法的动态规划将时间复杂度降为 O ( l o g N ) O(logN) O(logN)

1.博客地址
2.源代码仓库

如果你在代码里看到了用 数字标记的注释 如 //1.xxx 这是我写代码的顺序,希望能给你一点启发。


😁😁😁制作动画过程不易,请大家顺手点赞收藏咯 谢谢~😁😁😁
有其它题目不理解的也可以一起学习,如有错误欢迎指出~
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值