一、求斐波那契数列矩阵乘法的方法
1)斐波那契数列的线性求解(O(N))的方式非常好理解
2)同时利用线性代数,也可以改写出另一种表示
| F(N) , F(N-1) | = | F(2), F(1) | * 某个二阶矩阵的N-2次方
3)求出这个二阶矩阵,进而最快求出这个二阶矩阵的N-2次方
4)斐波那契数列 1 1 2 3 5 8 13.....其为fn = fn-1 + fn-2 最底层n-2 是二阶矩阵 n-2次方
其二阶矩阵为 1 1 也就是[ [1,1] [1,0] ]二维数组
1 0
二、类似斐波那契数列的递归优化
如果某个递归,除了初始项之外,具有如下的形式
F(N) = C1 * F(N) + C2 * F(N-1) + … + Ck * F(N-k) ( C1…Ck 和k都是常数)
并且这个递归的表达式是严格的、不随条件转移的
那么都存在类似斐波那契数列的优化,时间复杂度都能优化成O(logN)
先判断公式最底层阶数 i fn = fn-1...+ fn-i 依赖最低层的就是阶数 i阶 * 得到公式 : * |fn,fn-1,fn-2..fn-i+1| (共i项) = |fi,fi-1,fi-2...1| (共i项) * 矩阵i*i 的 n-i次方
三、斐波那契数列矩阵乘法方式的实现
1.斐波那契数列 1,1,2,3,5,8,13....
f(n) = f(n-1) + f(n-2)2.迈台阶问题 1,2,3,5,8,13,21....
f(n) = f(n-1) + f(n-2)3.铺瓷砖问题 1,2,3,5,8,13,21....
f(n) = f(n-1) + f(n-2)4.生牛问题 1,2,3,4,6,9,13,19...
f(n) = f(n-1) + f(n-3)
package class26;
/**
* 斐波那契数列矩阵乘法方式的实现
*/
public class FibonacciProblem {
/**
* 斐波那契数列
* @param n
* @return
*/
public static int f1(int n) {
if (n < 1) {
return 0;
}
if (n == 1 || n == 2) {
return 1;
}
return f1(n - 1) + f1(n - 2);
}
public static int f2(int n) {
if (n < 1) {
return 0;
}
if (n == 1 || n == 2) {
return 1;
}
int res = 1;
int pre = 1;
int tmp = 0;
for (int i = 3; i <= n; i++) {
tmp = res;
res = res + pre;
pre = tmp;
}
return res;
}
/**
* 总结一个 递推公式
* 斐波那契数列 : f(n) = f(n-1) + f(n-2) 最底层n-2 2阶 转换成矩阵乘法 降低时间复杂度
* |f(n),f(n-1)| = |f(2),f(1)| * 矩阵[[1,1],[1,0]]的n-2次方
*
* 其他同类型的固化公式: fn = 6*f(n-1) + 3* f(n-5) 最底层n-5 5阶 系数不影响公式 只影响矩阵统计
* |fn,fn-1,fn-2,fn-3,fn-4| = |f5,f4,f3,f2,f1| * 矩阵5*5 的 n-5次方
*
* 所以就是 先判断公式最底层阶数 i
* 得到公式 :
* |fn,fn-1,fn-2..fn-i+1| (共i项) = |fi,fi-1,fi-2...1| (共i项) * 矩阵i*i 的 n-i次方
*
* @param n
* @return
*/
// O(logN) 矩阵乘法方式的实现
public static int f3(int n) {
//边界判断 小于1 无效返回0
if(n < 1) return 0;
//初始1 2 首两个都是1
if(n == 1 || n ==2) return 1;
//因为斐波那契数列公式是 f(n) = f(n-1) + f(n-2) 依赖前2个 固定的一个公式 可以转换成
//|f(n) , f(n-1)| = |f(2) , f(1)| * 某个固定二阶矩阵的N-2次方 针对斐波那契数列
//这个固定二阶是因为依赖的最前面n-2 所有是一个二阶矩阵 也就是2*2二维数组 假设其他公式数列是到 n-3 那就是三阶数组3*3
//根据公式推算 这个二阶矩阵是[[1,1][1,0]]
//已知斐波那契数列 1,1,2,3,5,8....
//|F3,F2| = |F2,F1| * |A|矩阵
//|F4,F3| = |F3,F2| * |A|矩阵
//|F5,F4| = |F4,F3| * |A|矩阵...
//|FN,FN-1| = |FN-1,FN-2| * |A|矩阵
//进行推算 |F2,F1| = |1,1| |F3,F2| = |2,1| 假设|[a,b][cd]|矩阵
//根据矩阵计算方式 得到 |F3,F2| = |F2*a + F1*c , F2*b + F1*d| => F3=2= a+c F2=1= b + d 还需要一个等式才能算出a,b,c,d
//|F4,F3| = |F3*a + F2*c , F3*b + F2*d| => F4=3= 2a+c F3=2= 2b + d 四个等式 最后得出 a=1,b=1,c=1,d=0
//所以这个A矩阵就是
// a b =》 1 1
// c d 1 0
//根据每个等式都会包含前面一个值 比如|F4,F3| = |F3,F2| * 矩阵 F3 F2矩阵前面能通过 |F2,F1|* 矩阵得到
//直到FN,FN-1 我们就能化解成 = |F2,F1| * |A|矩阵 n-2 次方
//要得到FN 我们根据矩阵乘法运算 假设|A|矩阵 n-2 次方 得到一个值 [[a,b][c,d]]
//那么|FN,FN-1| = |1,1| * [[a,b],[c,d]] = |1*a+1*c, 1*b+1*d|
//求FN 就是 a+c FN-1 就是 b+d
// a就是 A矩阵位置[0][0] c 就是 A矩阵位置[1][0] 两个位置值相加即可
//根据分析 我们定义一个 矩阵a [[1,1][1,0] 求第n个值 那么就是 得到矩阵的 n-2次方的值
int[][] base = {{1,1},{1,0}};
//定义一个函数 将base 的n-2次方 值得到一个对应的矩阵数组 长度是一样的
int[][] res = getN(base,n-2);
return res[0][0] + res[1][0];
}
//获取这个二阶矩阵的 n次方 n = n-2 前面入参决定
public static int[][] getN(int[][] base, int n){
//这里有个技巧 如果快速运算 矩阵的n次方 我们需要利用二进制 将n次方转换成二进制数
//然后起初 我们让矩阵等于1 * base 1次方 2次方 4次方 8次方... 如果n =75 转换二进制 64 + 8 + 2 + 1 => 1001011
//根据有1的位置 就需要成上次方 0 的位置就跳过 矩阵 n的75次方 = 1 * 矩阵1次方 * 2次方 8 64
//矩阵值起始作为1 单位矩阵就可以作为1 因为其位置矩阵乘以任何数 都是=任何数 就是对角线为1 其他位置为0
//[[1,0],[0,1]] 单位矩阵 * [[1,1],[1,0]] 矩阵乘法运算
//
// a b * e f
// c d g h
//[[a,b][c,d]] * [[e,f][g,h]] = [a*e+b*g, a*f+b*h] [c*e+d*g, c*f+d*h]
//定义一个结果数组矩阵 base是自身相乘 所以结果矩阵大小是一致的
int row = base.length;
int col = base[0].length;
int[][] res = new int[row][col];
//初始化矩阵 为单位矩阵 然后接着再去乘以 base矩阵 单位矩阵就是对角线为1 任何矩阵乘以单位矩阵都是自身
//如果不初始化 一开始都是0 那乘以base结果都是0了
for(int r = 0; r < row; r++){
res[r][r] = 1; //因为矩阵是正方形的 所以就直接遍历行或列都可以 对角线赋值1
}
//base矩阵 n次方 我们利用n 的二进制位 如前面举例 n=75 => 1001011 最右侧开始位 与运算 1 判断位是否为1 为1 就表示要进行次方相乘 否则就无需相乘 跳过
//定义一个辅助数组存放base 用来进行累乘
int[][] temp = base;
//遍历次方 每个往右移动一位 也就是次方数n 每次都除以2 递减
for(; n != 0; n >>= 1){
if((n & 1) != 0){
//说明当前位是1, 需要进行temp相乘 刷新res 结果矩阵
res = getPower(res,temp);
}
//接着会右移一位 次方也需要相乘起来 接着要把辅助的矩阵也同步要往高位得到新的值 也就是需要 自身*自身 比如来到 第2位 那么矩阵就是2次方值 3位就是3次方值
temp = getPower(temp,temp);
}
return res;
}
//对两个矩阵进行相乘得到新矩阵值
//
// a b * e f
// c d g h
//[[a,b][c,d]] * [[e,f][g,h]] = [a*e+b*g, a*f+b*h] [c*e+d*g, c*f+d*h]
public static int[][] getPower(int[][] a, int[][] b){
int rowa = a.length; //矩阵a 行
int colb = b[0].length; //矩阵b 列
int cola = a[0].length; //矩阵a 列 根据矩阵运算的定义 a矩阵列 是跟b矩阵的行一致的 因为每次都是 某一行 a每列 要跟 b某一列 每行对应相乘 相加
//定义一个结果矩阵 行数对应a矩阵的行 列数对应b矩阵的列
int[][] res = new int[rowa][colb];
for(int ra = 0 ; ra < rowa; ra++){
for(int cb = 0; cb < colb; cb++){
for(int ca = 0; ca < cola; ca++){
//这里ca 列 等同于 b矩阵的 行 ,所以可以共用
//矩阵0,0 就等于 a矩阵第0行 b矩阵第0列 每个位置一一相乘 再一一累加 a[0][0]*b[0][0] + a[0][1]*b[1][0] + a[0][2]*b[2][0]...
//矩阵0,1 a矩阵第0行 b矩阵第1列...
//矩阵1,0 1 0...
res[ra][cb] += a[ra][ca] * b[ca][cb];
}
}
}
return res;
}
/**
* 一个人可以一次往上迈1个台阶,也可以迈2个台阶
*
* 返回这个人迈上N级台阶的方法数
*
*
* 用1*2的瓷砖,把N*2的区域填满
*
* 返回铺瓷砖的方法数
*
* 这两道都是一样的 1 2 3 5 8 13
* @param n
* @return
*/
public static int s1(int n) {
if (n < 1) {
return 0;
}
if (n == 1 || n == 2) {
return n;
}
return s1(n - 1) + s1(n - 2);
}
public static int s2(int n) {
if (n < 1) {
return 0;
}
if (n == 1 || n == 2) {
return n;
}
int res = 2;
int pre = 1;
int tmp = 0;
for (int i = 3; i <= n; i++) {
tmp = res;
res = res + pre;
pre = tmp;
}
return res;
}
public static int s3(int n) {
if (n < 1) {
return 0;
}
if (n == 1 || n == 2) {
return n;
}
int[][] base = { { 1, 1 }, { 1, 0 } };
int[][] res = getN(base, n - 2);
return 2 * res[0][0] + res[1][0];
}
/**
* 第一年农场有1只成熟的母牛A,往后的每年:
*
* 1)每一只成熟的母牛都会生一只母牛
*
* 2)每一只新出生的母牛都在出生的第三年成熟
*
* 3)每一只母牛永远不会死
*
* 返回N年后牛的数量
*
* 第一年只有1头母牛A
* 第二年 母牛A 生了母牛B 母牛B第三年成熟 成熟年可以生牛 也就是过多三年 第五年
* 第三年 母牛A 生了母牛C A,B,C C牛到第六年可以生牛
* 第四年 A生了D A,B,C,D
* 第五年 A生了E B到第三年了 生F A,B,C,D,E,F
* 第六年 A生了G B生了H C生了I A B C D E F G H I...
* 1 2 3 4 6 9 13...
*
* 得到 fn = fn-1 + fn-3 的关系式
* @param n
* @return
*/
public static int c1(int n) {
if (n < 1) {
return 0;
}
if (n == 1 || n == 2 || n == 3) {
return n;
}
return c1(n - 1) + c1(n - 3);
}
public static int c2(int n) {
if (n < 1) {
return 0;
}
if (n == 1 || n == 2 || n == 3) {
return n;
}
int res = 3;
int pre = 2;
int prepre = 1;
int tmp1 = 0;
int tmp2 = 0;
for (int i = 4; i <= n; i++) {
tmp1 = res;
tmp2 = pre;
res = res + prepre;
pre = tmp1;
prepre = tmp2;
}
return res;
}
public static int c3(int n) {
if (n < 1) {
return 0;
}
if (n == 1 || n == 2 || n == 3) {
return n;
}
//矩阵也是通过类推得到 a矩阵 看关系fn = fn-1 + fn-3 最底依靠n-3 3阶
//3阶 3*3矩阵 有以下关系类推得到矩阵的值
/**
* 1 2 3 4 6 9 13...
* |4,3,2| = |3,2,1| * [[a,b,c],[d,e,f],[g,h,i]]
* |6,4,3| = |4,3,2| * [[a,b,c],[d,e,f],[g,h,i]]
* |9,6,4| = |6,4,3| * [[a,b,c],[d,e,f],[g,h,i]]
*
* 通过这里 可以得出9个公式 九个位置就能算出 矩阵就是
* 1 1 0
* 0 0 1
* 1 0 0
*/
int[][] base = {
{ 1, 1, 0 },
{ 0, 0, 1 },
{ 1, 0, 0 } };
//i阶 次方就是 n-i 已知是3阶 所以就是n-3次方
int[][] res = getN(base, n - 3);
//返回fn 就是计算矩阵第一列 分别乘以 对应 |3 2 1| 初始的值
return 3 * res[0][0] + 2 * res[1][0] + res[2][0];
}
public static void main(String[] args) {
int n = 19;
System.out.println(f1(n));
System.out.println(f2(n));
System.out.println(f3(n));
System.out.println("===");
System.out.println(s1(n));
System.out.println(s2(n));
System.out.println(s3(n));
System.out.println("===");
System.out.println(c1(n));
System.out.println(c2(n));
System.out.println(c3(n));
System.out.println("===");
}
}
四、给定一个数N,想象只由0和1两种字符,组成的所有长度为N的字符串,如果某个字符串,任何0字符的左边都有1紧挨着,认为这个字符串达标,返回有多少达标的字符串
package class26;
/**
* 给定一个数N,想象只由0和1两种字符,组成的所有长度为N的字符串
*
* 如果某个字符串,任何0字符的左边都有1紧挨着,认为这个字符串达标
*
* 返回有多少达标的字符串
*
*
* 推算:
* N=1 1 1 不能为0,因为左边要有1
* N=2 2 11 10 10 符合0左边有1
* N=3 3 111 110 101 001 010不符合 每个0左边要有1
* N=4 5 1111 1110 1101 1011 1010
* N=5 8
* N=6 13...
* 得到是一个 fn = fn-1 + fn-2 数列 n=1||n=2 return n
*
* 尝试:
* 假设一个函数fn 还有n个数要形成字符 那么在n-1前面 肯定是1 因为最左边的只能是1 不能为0
* fn 范围就是 n,n-1,n-2...1 n个数
* 从左到右
* n位置如果选择了 1字符 那么后面n-1位置可以选择0或者1 fn递归依赖的就是 fn-1
* n位置如果选择了 0字符 那么后面n-1位置肯定只能选1 不能选0 因为要确保每个0左边都有1 所以n-1位置固定 来到n-2位置 可以选择0或者1 所以fn 依赖 fn-2
*
* 得到 fn = fn-1 + fn-2 公式
*
*
*
* 与以下题目也是一个推算公式:
* 用1*2的瓷砖,把N*2的区域填满
* 返回铺瓷砖的方法数
*
* 带入我们矩阵乘法方式的实现 O(logN) getNum3方法
*/
public class ZeroLeftOneStringNumber {
public static int t(int n) {
if (n == 0)
return 0;
if (n == 1 || n ==2)
return n;
return t(n - 1) + t(n - 2);
}
public static int getNum1(int n) {
if (n < 1) {
return 0;
}
return process(1, n);
}
public static int process(int i, int n) {
if (i == n - 1) {
return 2;
}
if (i == n) {
return 1;
}
return process(i + 1, n) + process(i + 2, n);
}
public static int getNum2(int n) {
if (n < 1) {
return 0;
}
if (n == 1) {
return 1;
}
int pre = 1;
int cur = 1;
int tmp = 0;
for (int i = 2; i < n + 1; i++) {
tmp = cur;
cur += pre;
pre = tmp;
}
return cur;
}
/**
* 总结一个 递推公式
* 斐波那契数列 : f(n) = f(n-1) + f(n-2) 最底层n-2 2阶 转换成矩阵乘法 降低时间复杂度
* |f(n),f(n-1)| = |f(2),f(1)| * 矩阵[[1,1],[1,0]]的n-2次方
*
* 其他同类型的固化公式: fn = 6*f(n-1) + 3* f(n-5) 最底层n-5 5阶 系数不影响公式 只影响矩阵统计
* |fn,fn-1,fn-2,fn-3,fn-4| = |f5,f4,f3,f2,f1| * 矩阵5*5 的 n-5次方
*
* 所以就是 先判断公式最底层阶数 i
* 得到公式 :
* |fn,fn-1,fn-2..fn-i+1| (共i项) = |fi,fi-1,fi-2...1| (共i项) * 矩阵i*i 的 n-i次方
*
* @param n
* @return
*/
// O(logN) 矩阵乘法方式的实现
public static int getNum3(int n) {
//边界判断 小于1 无效返回0
if(n < 1) return 0;
//初始1 2 首两个都是1
if(n == 1 || n ==2) return n;
//变型斐波那契数列 1 2 3 5 8 13 21...
//因为斐波那契数列公式是 f(n) = f(n-1) + f(n-2) 依赖前2个 固定的一个公式 可以转换成
//|f(n) , f(n-1)| = |f(2) , f(1)| * 某个固定二阶矩阵的N-2次方 针对斐波那契数列
//这个固定二阶是因为依赖的最前面n-2 所有是一个二阶矩阵 也就是2*2二维数组 假设其他公式数列是到 n-3 那就是三阶数组3*3
//根据公式推算 这个二阶矩阵是[[1,1][1,0]]
//已知斐波那契数列 1,1,2,3,5,8....
//|F3,F2| = |F2,F1| * |A|矩阵
//|F4,F3| = |F3,F2| * |A|矩阵
//|F5,F4| = |F4,F3| * |A|矩阵...
//|FN,FN-1| = |FN-1,FN-2| * |A|矩阵
//进行推算 此时是个变形的数列 |F2,F1| = |2,1| |F3,F2| = |3,2| 假设|[a,b][cd]|矩阵
//根据矩阵计算方式 得到 |F3,F2| = |F2*a + F1*c , F2*b + F1*d| => F3=3= 2a+c F2=2= 2b + d 还需要一个等式才能算出a,b,c,d
//|F4,F3| = |F3*a + F2*c , F3*b + F2*d| => F4=5= 3a+2c F3=3= 3b + 2d 四个等式 最后得出 a=1,b=1,c=1,d=0
//所以这个A矩阵就是
// a b =》 1 1
// c d 1 0
//根据每个等式都会包含前面一个值 比如|F4,F3| = |F3,F2| * 矩阵 F3 F2矩阵前面能通过 |F2,F1|* 矩阵得到
//直到FN,FN-1 我们就能化解成 = |F2,F1| * |A|矩阵 n-2 次方
//要得到FN 我们根据矩阵乘法运算 假设|A|矩阵 n-2 次方 得到一个值 [[a,b][c,d]]
//那么|FN,FN-1| = |2,1| * [[a,b],[c,d]] = |2*a+1*c, 2*b+1*d|
//求FN 就是 2a+c FN-1 就是 2b+d
// a就是 A矩阵位置[0][0] c 就是 A矩阵位置[1][0] 2*res[0][0] + res[1][0];
//根据分析 我们定义一个 矩阵a [[1,1][1,0] 求第n个值 那么就是 得到矩阵的 n-2次方的值
int[][] base = {{1,1},{1,0}};
//定义一个函数 将base 的n-2次方 值得到一个对应的矩阵数组 长度是一样的
int[][] res = getN(base,n-2);
return 2*res[0][0] + res[1][0];
}
//获取这个二阶矩阵的 n次方 n = n-2 前面入参决定
public static int[][] getN(int[][] base, int n){
//这里有个技巧 如果快速运算 矩阵的n次方 我们需要利用二进制 将n次方转换成二进制数
//然后起初 我们让矩阵等于1 * base 1次方 2次方 4次方 8次方... 如果n =75 转换二进制 64 + 8 + 2 + 1 => 1001011
//根据有1的位置 就需要成上次方 0 的位置就跳过 矩阵 n的75次方 = 1 * 矩阵1次方 * 2次方 8 64
//矩阵值起始作为1 单位矩阵就可以作为1 因为其位置矩阵乘以任何数 都是=任何数 就是对角线为1 其他位置为0
//[[1,0],[0,1]] 单位矩阵 * [[1,1],[1,0]] 矩阵乘法运算
//
// a b * e f
// c d g h
//[[a,b][c,d]] * [[e,f][g,h]] = [a*e+b*g, a*f+b*h] [c*e+d*g, c*f+d*h]
//定义一个结果数组矩阵 base是自身相乘 所以结果矩阵大小是一致的
int row = base.length;
int col = base[0].length;
int[][] res = new int[row][col];
//初始化矩阵 为单位矩阵 然后接着再去乘以 base矩阵 单位矩阵就是对角线为1 任何矩阵乘以单位矩阵都是自身
//如果不初始化 一开始都是0 那乘以base结果都是0了
for(int r = 0; r < row; r++){
res[r][r] = 1; //因为矩阵是正方形的 所以就直接遍历行或列都可以 对角线赋值1
}
//base矩阵 n次方 我们利用n 的二进制位 如前面举例 n=75 => 1001011 最右侧开始位 与运算 1 判断位是否为1 为1 就表示要进行次方相乘 否则就无需相乘 跳过
//定义一个辅助数组存放base 用来进行累乘
int[][] temp = base;
//遍历次方 每个往右移动一位 也就是次方数n 每次都除以2 递减
for(; n != 0; n >>= 1){
if((n & 1) != 0){
//说明当前位是1, 需要进行temp相乘 刷新res 结果矩阵
res = getPower(res,temp);
}
//接着会右移一位 次方也需要相乘起来 接着要把辅助的矩阵也同步要往高位得到新的值 也就是需要 自身*自身 比如来到 第2位 那么矩阵就是2次方值 3位就是3次方值
temp = getPower(temp,temp);
}
return res;
}
//对两个矩阵进行相乘得到新矩阵值
//
// a b * e f
// c d g h
//[[a,b][c,d]] * [[e,f][g,h]] = [a*e+b*g, a*f+b*h] [c*e+d*g, c*f+d*h]
public static int[][] getPower(int[][] a, int[][] b){
int rowa = a.length; //矩阵a 行
int colb = b[0].length; //矩阵b 列
int cola = a[0].length; //矩阵a 列 根据矩阵运算的定义 a矩阵列 是跟b矩阵的行一致的 因为每次都是 某一行 a每列 要跟 b某一列 每行对应相乘 相加
//定义一个结果矩阵 行数对应a矩阵的行 列数对应b矩阵的列
int[][] res = new int[rowa][colb];
for(int ra = 0 ; ra < rowa; ra++){
for(int cb = 0; cb < colb; cb++){
for(int ca = 0; ca < cola; ca++){
//这里ca 列 等同于 b矩阵的 行 ,所以可以共用
//矩阵0,0 就等于 a矩阵第0行 b矩阵第0列 每个位置一一相乘 再一一累加 a[0][0]*b[0][0] + a[0][1]*b[1][0] + a[0][2]*b[2][0]...
//矩阵0,1 a矩阵第0行 b矩阵第1列...
//矩阵1,0 1 0...
res[ra][cb] += a[ra][ca] * b[ca][cb];
}
}
}
return res;
}
public static void main(String[] args) {
for (int i = 0; i != 20; i++) {
System.out.println(getNum1(i));
System.out.println(getNum2(i));
System.out.println(getNum3(i));
System.out.println(t(i));
System.out.println("===================");
}
}
}

本文介绍了使用矩阵快速幂技巧优化斐波那契数列的计算,详细阐述了矩阵乘法在求解斐波那契数列中的应用,将时间复杂度降至O(logN)。此外,还探讨了类似斐波那契数列结构的递归优化方法,并列举了矩阵乘法在不同问题(如迈台阶问题、铺瓷砖问题、生牛问题)中的应用。最后提出了一种字符串匹配问题,寻找由0和1组成的字符串中,所有0字符左侧都有1紧邻的情况。
8万+

被折叠的 条评论
为什么被折叠?



