引言
1.斐波那契数列
题目:初始状态 ,斐波那契数列问题就是函数的状态转移问题: 。
暴力递归:
- 采用系统压栈的方式完成,时间复杂度为: ,复杂度为:,严格上是:
/**
* 斐波那契问题:递归解法
* 时间复杂度:O(2^n)
*/
public static int fb1(int n) {
if (n < 1) return 0;
if (n == 1 || n == 2) return 1;
return fb1(n - 1) + fb1(n - 2);
}
动态规划1.0:
- 很多地方将动态规划,上来搬出的第一个例子就是 斐波那契数列问题,将它的 暴力递归解法 与 动态规划解法进行对比,突显动态规划复杂度上的优势。但是,暴力递归是动态规划的前提,你只有理解了暴力递归的尝试解法,才能顺利成章的推导出它的动态规划解法。
- 所有的动态规划都是可以由暴力递推推到而来,反之则不一定,它们的包含关系如下:
- 这篇文章不着重讲解暴力递归改动态规划的基本套路,后面会写专门的文章来讲解。
- 在递归函数 中,只有一个可变参数,对应不同的输入,对应一个固定的输出,是用一张一维表就能装下所有结果。
- 定义: 表示 的结果,那么 , 这就是动态规划的转移方程,动态规划的推到过程,就是填写 表的过程。
- 时间复杂度: ,空间复杂度
/**
* 暴力递归改动态规划,一个可变参数,对应一维dp表
* 动态规划的过程,本质上就是填写 dp 表的过程
* 根据递推公式,找到 dp 表的以来关系,将 dp 表填写完成
*/
public static int dp(int n){
if (n <= 0) return 0;
if (n <= 2) return 1;
//1.有暴力递归可知,可变参数取值为[0,n]
int[] dp = new int[n + 1];
//2.下面来填写dp表,先填写初始位置,对应暴力递归的:if (n == 1 || n == 2) return 1;
dp[1] = 1;
dp[2] = 1;
//3.再填写普遍位置
for (int i = 3; i <= n ; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
动态规划2.0:
- 在填写 表过程中,发现当前位置 只依赖它的第 和 位置,那么只需要用两个变量 来记录前两个位置,通过滚动更新两个变量就能完成动态规划的过程。省略掉 数组。
- 这个思路就是动态规划中的空间压缩技巧,时间复杂度 ,空间复杂度 。
- 所以我们通常看到的斐波那契数列动态规划代码如下,用到空间压缩技巧:
/**
* 动态规划2.0版本:空间压缩
*/
public static int dp2(int n) {
if (n <= 0) return 0;
if (n <= 2) return 1;
int dpi_1 = 1;
int dpi_2 = 1;
for (int i = 3; i <= n; i++) {
int temp = dpi_1 + dpi_2;
dpi_1 = dpi_2;
dpi_2 = temp;
}
return dpi_1;
}
在后续的动态规划文章中,会重点温习暴力递归改动态规划的基本套路,及空间压缩等技巧。
接下来要讲解的是 斐波那契问题的 的解法。
2.斐波那契 解法
要解决斐波那契问题 解法,首先需要了解以下几个算法原型:
- 快速幂,该算法能在 时间复杂度内求解 问题。
- 线性代数基础,向量与矩阵的乘法问题。
快速幂
- 求的 问题,暴力解是通过将 次。事件复杂度为
public static int quickMi(int x,int n){
//未做输入参数检验
int sum = 1;
while (n != 0){
sum *= x;
n--;
}
return sum;
}
快速幂步骤:
- 中,可以将 看成二进制的形式, 中, 。
- 注意 中只有二进制位 的位置乘积才有结果:
- 那么问题就变成了判断 的二进制表示中,从右往左哪些位置上的值是 ,将 乘到结果上去。
public static int quickPow(int x,int n){
int sum = 1;
while (n != 0){
//二进制中最后一个位置是否为1,是1就乘上去,是 0 就没必要
if ((n & 1) == 1){
sum *= x;
}
//n无符号右移
n >>>= 1;
x *= x;
}
return sum;
}
斐波那契问题与快速幂
斐波那契数列:
对于一个状态,只有初始态通过前提条件得到状态值,后续的所有状态都不受条件判断的印象,为严格的递推函数,则它有线性代数的解。
先给出结论:这是一个二阶矩阵
- =>
- 向量与矩阵的乘积计算,通过初始几个值: ,就变成求解四元一次方程了,手动计算出该矩阵: 。
问题就转变成了求 矩阵 问题,这不就是快速幂么,只是底数变成了一个矩阵。
代码:
/**
* 斐波那契数列 logn 解法
* 二阶递推:|a,b|
* |c,d|
*/
public static int fb3(int n) {
if (n < 1) return 0;
if (n == 1 || n == 2) return 1;
//1.定义二阶矩阵,通过手动计算出来
int[][] base = {{1, 1}, {1, 0}};
//2.求出 base^(n-2)次方
int[][] res = matrixPower(base, n - 2);
//3. |f(n),f(n-1)|=|f(2),f(1)|* res = a * f(2) + c * f(1)
return res[0][0] + res[1][0];
}
/**
* 矩阵的n次方,快速幂的方法
*/
public static int[][] matrixPower(int[][] m, int n) {
int[][] res = new int[m.length][m[0].length];
//1.构造单位矩阵
for (int i = 0; i < m.length; i++) {
res[i][i] = 1;
}
//2.快速幂求矩阵的n次方
int[][] temp = m;
for (; n != 0; n >>= 1) {
//3.如果当前二进制位是1,则相乘
if ((n & 1) != 0) {
res = multiMatrix(res, temp);
}
temp = multiMatrix(temp, temp);
}
return res;
}
/**
* 定义两个矩阵的相乘
*/
public static int[][] multiMatrix(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 < m2.length; k++) {//列数
res[i][j] += m1[i][k] * m2[k][j];
}
}
}
return res;
}
3.斐波那契问题推广
对于一个递推公式: ,其中 都为常数,称为 阶递推式。
- 阶对应的矩阵就是 阶矩阵
- 以三阶为例:
总结:除了初始状态可以根据条件进行设置,后续所有状态都不能根据条件来递推,满足严格递推才能用。
奶牛繁殖问题
开始有一只奶牛,并且保证奶牛永远不会死,每只奶牛过三年后可以生一只奶牛,请问 年后的奶牛数量。
那么该递推公式就是:
- 这是一个三阶问题,对应的矩阵是
/**
* 奶牛繁殖问题:
* 1,2,3,4,6,9
* f(n) = f(n-1) + f(n-3)
*/
public static int cowPb(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] + res[2][0];
}
注意:
如果题目规定奶牛10年后会死掉,则递推公式变为:
贴瓷砖问题
题目描述:给定一块矩阵面积为 ,瓷砖的规格只有 大小,问有多少种不同的铺砖方法?
递推公式:
青蛙跳台阶问题
爬楼梯
字符达标问题
给定一个数 , 设定只由 和 两种字符,组成的所有长度为 的字符串,如果某个字符串,任何 字符的左边都要紧挨着 ,认为这个字符串达标,返回有多少个达标的字符串?
当 时候:
思路:
- 1、所有合法的字符都不可能以 开头( 的左边必须紧挨着 ),只能以 开头, 所以此时就是 的达标数量。
- 2、开头是 ,剩下的 达标的数量。
- 3、总的数量即为