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 ...
随机推荐
- Salem and Sticks-萨鲁曼的棍子 CodeForce#1105A 暴力
题目链接:Salem and Sticks 题目原文 Salem gave you
- selenium-04-验证码问题
对于web应用来说,大部分的系统在用户登录时都要求用户输入验证码,验证码的类型的很多,有字母数字的,有汉字的,甚至还要用户输入一条算术题的答案的,对于系统来说使用验证码可以有效果的防止采用机器猜测方法 ...
- Linux版本号的数值含义
Linux内核版本有两种:稳定版和开发版 ,Linux内核版本号由3组数字组成:第一个组数字.第二组数字.第三组数字.第一个组数字:目前发布的内核主版本.第二个组数字:偶数表示稳定版本:奇数表示开发中 ...
- js 跳转链接的几种方式
1.跳转链接 在当前窗口打开 window.location.href="http://www.baidu.com" 等价于 <a href="baidu.com& ...
- Logrotate配置
目录 Logrotate配置 参考 Logrotate Description Logrotate Configuration Logrotate配置
- 一文搞定 SonarQube 接入 C#(.NET) 代码质量分析
1. 前言 C#语言接入Sonar代码静态扫描相较于Java.Python来说,相对麻烦一些.Sonar检测C#代码时需要预先编译,而且C#代码必须用MSbuid进行编译,如果需要使用SonarQub ...
- MongoDB 学习笔记之 replica set搭建
Replica set搭建: 修改mongodb.conf文件,指明replSet 登入客户端,指定副本集成员,进行初始化, 如果priority需要调整,使用reconfig()方法.Seconda ...
- MongoDB 学习笔记之 游标
游标: 游标是查询的接口,可以逐条读取. var mycursor = db.bar.find(); mycursor.hasNext(); mycursor.next(); 示例: var mycu ...
- Spring Boot 2.X(五):MyBatis 多数据源配置
前言 MyBatis 多数据源配置,最近在项目建设中,需要在原有系统上扩展一个新的业务模块,特意将数据库分库,以便减少复杂度.本文直接以简单的代码示例,如何对 MyBatis 多数据源配置. 准备 创 ...
- Vue学习系列(一)——初识Vue.js核心
前言 vue.js是一套构建用户界面的渐进式框架,vue.js的目标是通过尽可能简单的API实现响应的数据绑定和组合的视图组件. vue通过DOM事件操作和指令来进行视图层和模型层的相互通讯,会为每一 ...