OptimalSolution(1)--递归和动态规划(1)斐波那契系列问题的递归和动态规划
一、斐波那契数列
斐波那契数列就是:当n=0时,F(n)=0;当n=1时,F(n)=1;当n>1时,F(n) = F(n-1)+F(n-2)。
根据斐波那契数列的定义,斐波那契数列为(从n=1开始):1,1,2,3,5,8...,也就是除了第1项和第2项外,对于第N项,都有F(n) = F(n-1)+F(n-2)
1.时间复杂度为O(2n)的暴力递归算法
暴力递归算法是最慢的一种方法,加入要计算F(10)的值,那么就要计算F(9)和F(8)的值,而计算F(9)的值需要计算F(8)和F(7)的值,计算F(8)就需要计算F(7)和F(6)的值...这样就会造成多次的重复计算,例如F(8)和F(7)就要计算多次。递归算法虽然代码简单容易理解,但是效率低下,时间复杂度随n的值指数增长。
public int fib1(int n) {
if (n < 1) return 0;
if (n == 1 || n == 2) return 1;
return fib1(n - 1) + fib1(n - 2);
}
2.时间复杂度为O(n)的非递归算法
考虑到递归算法在每一步的计算中都重复地计算了很多不必要的值,因此问题就是如何避免计算那些重复的值。由于可以从左到右利用前面的两个值计算出每一项的值,然后通过顺序累加就可以求出第N项的值。
public int fib2(int n) {
if (n < 1) return 0;
if (n == 1 || n == 2) return 1;
int first = 1;
int second = 1;
int tmp = 0;
for (int i = 3; i <= n; i++) {
tmp = second;
second = first + second;
first = tmp;
}
return second;
}
3.时间复杂度为O(logn)的状态矩阵乘法算法
由于F(n) = F(n-1)+F(n-2)是二阶递推数列,而二阶递推数列一定可以用矩阵乘法的形式表示,且状态转移矩阵为2×2的矩阵。
(F(n), F(n-1)) = (F(n-1), F(n-2)) x {{a,b},{c,d}}(矩阵)
将F(1)=1,F(2)=1,F(3)=2,F(4)=3带入可以求出状态矩阵a=1,b=1,c=1,d=0
(F(3),F(2))=(F(2),F(1)) × base
(F(4),F(3))=(F(3),F(2)) × base = (F(2),F(1)) × base × base = (F(2),F(1)) × base2
(F(4),F(3))=(F(2),F(1)) × base3
...
(F(n), F(n-1)) = (F(n-1), F(n-2)) x {{1,1},{1,0}} = ... = (F(2),F(1))x {{1,1},{1,0}}
(n-2)
于是,求斐波那契数列第N项的问题就变成了如何用最快的方法求一个矩阵的N次方的问题,而求一个矩阵的N次方的问题明显是一个能够在O(logn)时间内解决的问题。
假设要求一个整数的N次方应该如何求解,然后将求整数的N次方的思路应用到求矩阵的N次方就可以了。
如何最快求解10的75次方:
1.75的二进制形式为1001011
2.10的75次方为10^64×10^8×10^2×10^1
于是,首先求出10的1次方,然后根据10的1次方求10的2次方,然后根据10的2次方求10的4次方...以此类推,而64/8/2/1正好对应着75的二进制中1所在的位置表示的数。
对于矩阵的N次方也是如此,例如,muliMatrix方法是矩阵m1和m2相乘,而matrixPower方法是矩阵m的p次方。
- 矩阵乘法函数
public int[][] muliMatrix(int[][] m1, int[][] m2){
int[][] res = new int[m1.length][m2[0].length];
for (int i = 0; i < m1.length; i++) {
for (int j = 0; j < m2[0].length; j++) {
for (int k = 0; k < m1[0].length; k++) {
res[i][j] += m1[i][k] * m2[k][j];
}
}
}
return res;
}
- 矩阵乘方函数
public int[][] matrixPower(int[][] m, int p){
int[][] res = new int[m.length][m[0].length];
// 先把res设为单位矩阵,相当于整数中的1
for (int i = 0; i < res.length; i++) res[i][i] = 1;
int[][] tmp = m;
for (; p != 0; p >>= 1) {
if ((p & 1) != 0) {
res = muliMatrix(res, tmp);
}
tmp = muliMatrix(tmp, tmp);
}
return res;
}
和求10的75次方类似,假设要求矩阵m的75次方,那么代码的执行过程是,
首先,75的二进制是:1001011,res为单位矩阵,相当于整数里面的1,
然后p为1001011,tmp = m的1次方,res 为单位矩阵
最右一位是1,res = res * tmp = m的1次方,tmp = m的2次方
最右一位是1,res = res * tmp = m的1+2次方,tmp = m的4次方
最右一位是0,tmp = m的8次方
最有一位是1,res = res * tmp = m的1+2+8次方,tmp = m的16次方
最右一位是0,tmp = m的32次方
最右一位是0,tmp = m的64次方
最右一位是1,res = res * tmp = m的1+2+8+64=75次方,tmp = m的128次方,p == 0, return res即为m的75次方
- 使用矩阵乘法求解斐波那契第N项的主函数
public int fib3(int n) {
if (n < 1) return 0;
if (n == 1 || n == 2) return 1;
int[][] base = {{1,1}, {1,0}};
int[][] res = matrixPower(base, n - 2);
return res[0][0] + res[1][0];
}
其中,res的结果就是状态矩阵base的n-2次方,而根据(F(n), F(n-1)) = (F(2),F(1)) x base(n-2) = (1,1) x base(n-2) 就可以知道F(n)就是1*res[0][0] + 1*res[1][0]
二、爬台阶问题
问题:给定整数N,代表台阶数,一次可以爬2个或1个台阶,返回有多少种爬法?例如:N=3,可以1,1,1,也可以2,1,还可以1,2,一共有3种走法,所以返回3。
分析过程:如果台阶只有1级,那么方法只有一种;如果台阶有2级,方法有两种;如果台阶有N级,那么最后一步爬上台阶的方法,要么是从N-2级台阶直接爬到第N级台阶,要么是从N-1级台阶爬到第N级台阶,所以爬上N级台阶的方法数就等于爬上N-2级台阶的方法数加上爬上N-1级台阶的方法数,即F(N) = F(N-1) + F(N-2),初始项F(1)=1,F(2)=2。可以看出,这和斐波那契数列类似,唯一不同的就是初始项不同,同样根据前4项:1,2,3,5求出状态矩阵(两组方程组,4个等式可以解出4个未知数)base,然后根据(F(n), F(n-1)) = (F(2),F(1)) x base(n-2) = (2,1) x base(n-2) 就可以求出当给定台阶数为N时,一共有多少种爬法。
三、生小牛问题
问题:假设农场中成熟的母牛每年只会生1头小母牛,并且永远不会死。第一年农场只有1只成熟的母牛,从第二年开始,母牛开始生小母牛。每只小母牛3年之后成熟。给定整数N,求出N年后牛的数量。
分析生小牛的过程:1,2,3,4,6,9...N=7时,第7年的总牛数等于第6年的总牛数加上第7年成熟的母牛数,第7年成熟的母牛数又是第4年的总牛数,因此9+4=13
也就是:第N-1年的所有牛都会活到第N年,且第N-3年的所有牛到第N年肯定都是成熟的牛,因此F(N)=F(N-1)+F(N-3),初始项为F(1)=1,F(2)=2,F(3)=3
N=1,第1年有1头成熟母牛,记为a,总牛数为1
N=2,第2年有1头成熟母牛a,和a新生的小牛,记为b,总牛数为2
N=3,第3年有1头成熟母牛a,和a新生的小牛,记为c,小牛b,c,总牛数为3
N=4,第4年有1头成熟母牛a,和a新生的小牛,记为d,小牛b,c,d,总牛数为4
N=5,第5年有2头成熟母牛a,b,和a,b新生的小牛,记为e,f,小牛c,d,e,f,总牛数为6
N=6,有3头成熟母牛a,b,c,和a,b,c新生的小牛,记为g,h,i,小牛c,e,f,g,h,i总牛数为9 ...
(1)时间复杂度为O(2n)的递归算法
public int cow1(int n) {
if (n < 1) return 0;
if (n == 1 || n == 2 || n == 3) return n;
return cow1(n - 1) + cow1(n - 3);
}
(2)时间复杂度为O(n)非递归算法
public int cow2(int n) {
if (n < 1) return 0;
if (n == 1 || n == 2 || n == 3) return n;
int first = 1, second = 2, third = 3;
for (int i = 4; i <= n; i++) {
int tmp = second;
second = third;
third = first + third;
first = tmp;
}
return third;
}
(3)时间复杂度为O(logn)的状态矩阵乘法算法
根据F(N)=F(N-1)+F(N-3)是一个三阶递推数列,可以知道,一定可以用矩阵乘法表示,而且状态矩阵为3×3的矩阵base。
即(F(N),F(N-1),F(N-2))= (F(N-1),F(N-2),F(N-3))× base
把F(1)=1,F(2)=2,F(3)=3,F(4)=4,F(5)=6,带入上面的等式中,可以解得base={{1,1,0},{0,0,1},{1,0,0}}
然后有当n>3时,
n=4,(F(4),F(3),F(2))= (F(3),F(2),F(1))× base1
n=5,(F(5),F(4),F(3))= (F(4),F(3),F(2))× base1 = (F(3),F(2),F(1))× base2
n=6,(F(6),F(5),F(4))= (F(5),F(4),F(3))× base1 = (F(3),F(2),F(1))× base3
...
n=n,(F(n),F(n-1),F(n-2))= (F(3),F(2),F(1))× basen-3
于是,有使用状态矩阵乘法的算法为:
public int cow3(int n) {
if (n < 1) return 0;
if (n == 1 || n == 2 || n == 3) return n;
int[][] base = {{1,1,0},{0,0,1},{1,0,0}};
int[][] res = matrixPower(base, n - 3);
return 3 * res[0][0] + 2 * res[1][0] + 1 * res[2][0];
}
四、总结
如果递归式严格符合F(n) = a×F(n-1) + b×F(n-2) + c×F(n-3) + ... + k×F(n-i),那么此递归式就是一个i阶的递归式,一定可以写成i×i的状态转移矩阵表达的形式,一律可以用状态转移矩阵乘法然后乘方的方法实现时间复杂度为O(logn)的动态规划算法。
OptimalSolution(1)--递归和动态规划(1)斐波那契系列问题的递归和动态规划的更多相关文章
- C语言数据结构----递归的应用(斐波拉契数列、汉诺塔、strlen的递归算法)
本节主要说了递归的设计和算法实现,以及递归的基本例程斐波拉契数列.strlen的递归解法.汉诺塔和全排列递归算法. 一.递归的设计和实现 1.递归从实质上是一种数学的解决问题的思维,是一种分而治之的思 ...
- JS 从斐波那契数列浅谈递归
一.前言 昨晚下班后,经理出于兴趣给我们技术组讲了讲算法相关的东西,全程一脸懵逼的听,中途还给我们出了一道比较有趣的爬楼问题,问题如下: 假设一个人从地面开始爬楼梯,规定一步只能爬一坎或者两坎,人只能 ...
- Python递归 — — 二分查找、斐波那契数列、三级菜单
一.二分查找 二分查找也称之为折半查找,二分查找要求线性表(存储结构)必须采用顺序存储结构,而且表中元素顺序排列. 二分查找: 1.首先,将表中间位置的元素与被查找元素比较,如果两者相等,查找结束,否 ...
- JS:递归基础及范例——斐波那契数列 、 杨辉三角
定义:程序调用自身的编程技巧称为递归.一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的程序就 ...
- 斐波那契数列PHP非递归数组实现
概念: 斐波那契数列即表达式为 a(n) = a(n-1)+a(n-2) 其中 a1 =0 a2 = 1 的数列 代码实现功能: 该类实现初始化给出n,通过调用getValue函数得出a(n)的值 ...
- golang中通过递归或通道实现斐波那契数列
1. 循环实现 package main import "fmt" func fibonacciFor(nums int) (s1 []int) { // 循环实现斐波那切数列 n ...
- C#递归、动态规划计算斐波那契数列
//递归 public static long recurFib(int num) { if (num < 2) ...
- java 递归打印20个斐波那契数
class Test { public static void main(String[] args) { // feibo j=new feibo(); for (int n = 1; n < ...
- python 递归\for循环_斐波那契数列
# 递归 def myAdd(a, b): c = a + b print(c) if c > 100: return return myAdd(a + 1, c) #最大递归深度是1000 m ...
随机推荐
- Windows认证 | 域认证
在Windows中的身份认证方式有很多,也在不断的升级,但是在域中,依旧使用的是Kerberos认证. Kerberos 是一种网络认证协议,它的实现不依赖于主机操作系统的认证,无需基于主机地址的信任 ...
- Linux 中文打字软件 gtypist 光标错位解决
在windows 下有 金山打字和其他的跟打软件,在Linux下找到了 gtypist 为练习中文打字,该软件分为练习模式的速度测试模式,在gtypist-2.9.5版中会出现以下几个问题: 一是在练 ...
- 常见MySQL数据库语句
##############Author: Fan ################# (1)数据库 # 查看所有的数据库 SHOW DATABASES ; # 创建一个数据库 ...
- 杭州蓝松科技---短视频SDK介绍
蓝松短视频的口号和 更新周期: 我们的口号是: 蓝松短视频 任意个性化. 我们是杭州蓝松科技, 专业做视频短视频SDK的技术团队. 我们提供 Android/IOS平台上的 短视频编辑SDK, ...
- 根据vue-cli手摸手实现一个自己的脚手架
故事背景 身为一个入门前端七个月的小菜鸡,在我入门前端的第一天就接触到了vue,并且死皮赖脸的跟他打了这么久的交到,还记得第一次用vue init webpack 这句命令一下生成一个模板的时候那种心 ...
- Kafka常用命令合集
在上一篇文章<Linux安装Kafka>中,已经介绍了如何在Linux安装Kafka,以及Kafka的启动/关闭和创建发话题并产生消息和消费消息.这篇文章就介绍介绍Kafka的那些常用的命 ...
- 使用gtest(googletest)进行c++单元测试
这是系列文章的第三篇,前两篇https://www.cnblogs.com/gaopang/p/11243367.html和https://www.cnblogs.com/gaopang/p/1158 ...
- 品Spring:对@PostConstruct和@PreDestroy注解的处理方法
在bean的实例化过程中,也会用到一系列的相关注解. 如@PostConstruct和@PreDestroy用来标记初始化和销毁方法. 平常更多的是侧重于应用,很少会有人去了解它背后发生的事情. 今天 ...
- v-text和v-html的区别
一.v-text 用于渲染普通文本,无论何时,绑定的数据对象上 msg属性发生了改变,插值处的内容都会更新. <span v-text="message"></s ...
- vue多级复杂列表展开/折叠,全选/分组全选实现
首先,来看下效果图 在线体验地址:https://hxkj.vip/demo/multipleList/.温馨提示,打开之后按F12,使用手机模式食用,口味更佳! 可以看出,这个列表有三种展现形式: ...