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 ...
随机推荐
- JavaScript总结(一)
一.JavaScript 简介 1.1 .什么是 JavaScript? JavaScript 的简称:JS. JavaScript 是一个脚本.(不需要经过编译器编译的语言就叫做脚本) JavaSc ...
- Enum枚举的使用实现
业务中涉及到的状态字段或者简单的选择项的使用. 例如: 1.定义enum枚举类. package com.yjl.enums; import java.util.Objects; public enu ...
- dede tag标签静态化
看回那2个文件夹即可,txt说明书我已经修改过. 下面说一下tag标签静态化之后在内容页.列表页中如何使用. 内容页中沿用之前的方法即可: {dede:tag sort='new' getall='0 ...
- ARP攻击原理简析及防御措施
0x1 简介 网络欺骗攻击作为一种非常专业化的攻击手段,给网络安全管理者,带来严峻的考验.网络安全的战场已经从互联网蔓延到用户内部的网络, 特别是局域网.目前利用ARP欺骗的木马病毒在局域网中广泛传 ...
- 【MySQL】 用户授权
启动mysql命令符 grant all privileges on mysql.* to 'root'@'%' identified by '123456'; 给mysql用户root授权,'%'表 ...
- SSH实现无密码登录
1.生成秘钥 : ssh-keygen -t rsa # -t 指定生成秘钥方式,生成秘钥过程需要三次回车 2.将生成的公钥传给 ssh 的对端 ssh-copy-id root@192.168.3. ...
- day 21
目录 组合 封装 访问机制 property 多态 抽象类的目的 鸭子类型 组合 组合是指的是一个对象中的属性,时另一个对象. 组合的目的和继承一样,为了减少代码冗余 封装 封装指的是把一堆属性(特征 ...
- Window下的VScode快捷键
转载自4ark 全局 Ctrl + Shift + P, F1 显示命令面板 Ctrl + P 快速打开Ctrl + Shift + N 打开新窗口Ctrl + Shift + W 关闭窗口 基本 C ...
- 一文了解 Redis 内存监控和内存消耗
Redis 是一种内存数据库,将数据保存在内存中,读写效率要比传统的将数据保存在磁盘上的数据库要快很多.所以,监控 Redis 的内存消耗并了解 Redis 内存模型对高效并长期稳定使用 Redis ...
- Cutting Sticks UVA - 10003
题文: 见:https://uva.onlinejudge.org/index.php?option=com_onlinejudge&Itemid=8&page=show_proble ...