这是我参与11月更文挑战的第19天,活动详情查看:2021最后一次更文挑战。
再试
斐波那契数列的二阶的矩阵
那么就要求出斐波那契数列的二阶的矩阵 A 了。
已知,F(3) = F(2) + F(1),F(4) = F(3) + F(2) = 2F(2) + F(1), F(2) = 1,F(1) = 1。
代入二阶常系数齐次线性递推数列的规律,得:
|F(3) F(2)| = |F(2) F(1)| * A
|F(4) F(3)| = |F(3) F(2)| * A
这里的|F(2) F(1)| 和|F(3) F(2)|也是一个矩阵,
二阶矩阵 A 为
||a b|,
|c d||
我们运算时把 F(3)、F(2)、F(1) 都转换成数字 2、1、1。
|F(3) F(2)| = |1 1| * A = | a + c , b + d|
|F(4) F(3)| = |2 1| * A = | 2a + c, 2b + d |
于是我们得出四条方程式:
a + c = 2, b + d = 1, 2a + c = 3, 2b + d = 2.
计算得出
a = 1, b = 1, c = 1, d = 0.
于是我们的矩阵 A 为
||1 1|,
|1 0||
有了矩阵 A,我们就可以通过移动比特位的方式极大地减少时间复杂度,也就是达到 log(n)。
那么最后一个问题来了,初始的基数是啥?
初始的基数
如果是求 X 的 N 次方,那么我们可以让调用者来输入,但这里是斐波那契数列啊,我们就得把基数确定下来。
我们依照二阶常系数齐次线性递推数列的规律:|F(n) F(n-1)| = |F(n-1) F(n-2)| * A,就可以得出我们的基数使用的就是|F(2) F(1)|。
But,我们还要创建一个矩阵相乘的方法,如果使用基数:|F(2) F(1)|,那我们就无法写一个能够通用的方法,所以我们同样要找一个二阶的矩阵作为基数。
于是乎,可以使用 ||F(2) F(1)|,|0 0|| 来作为基数,我们只取第一列的值就行。
好了,开始敲代码。
/**
* 时间复杂度O(log(n))
*
* @param n
* @return
*/
public int fibonacci(int n) {
if (n <= 2) {
return 1;
}
int[][] base = {{1, 1}, {0, 0}};
int[][] A = {{1, 1}, {1, 0}};
base = multiMatrix(base, A, n - 2);
return base[0][0];
}
/**
* base 矩阵 和 A 矩阵的 n 次方相乘
*
* @param base
* @param A
* @param n
* @return
*/
private int[][] multiMatrix(int[][] base, int[][] A, int n) {
//这里就可以模仿 calculationPower1 了,只不过原来的 temp = base,这里就改成了 = A,
// 因为不是自乘了,而是跟矩阵 A 相乘
int[][] temp = A;
for (; n != 0; n = n >> 1) {
if ((n & 1) != 0) {
base = multiMatrix(base, temp);
}
temp = multiMatrix(temp, temp);
}
return base;
}
/**
* 两个矩阵相乘
*
* @param base
* @param A
* @return
*/
private int[][] multiMatrix(int[][] base, int[][] A) {
int[][] ints = new int[base.length][base[0].length];
for (int i = 0; i < ints.length; i++) {
for (int j=0;j<ints[0].length;j++){
for (int k=0;k<A[0].length;k++){
ints[i][j] += base[i][k] * A[k][j];
}
}
}
return ints;
}
因为个人水平有限,两个矩阵相乘的方法呢,我是抄了别人的写法,然后经过自己的理解后稍微修改了一部分,我无法从矩阵相乘推算出算法,这里只能结合算法和矩阵相乘,推出这个算法到底是怎么算的。
还记得之前说的矩阵的乘法运算吗?
|
|a1 , a2|,
|a3 , a4|
| × |
|b1 , b2|,
|b3 , b4|
| = |
|a1 b1 + a2 b3 , a1 b2 + a2 b4|,
|a3 b1 + a4 b3 , a3 b2 + a4 b4||
这里我们把 an 看成数组 a,把 bn 看成数组 b,最后面这个 |a1 b1 + a2 b3 ...| 看成数组 res。
res =
|a1 b1 + a2 b3 , a1 b2 + a2 b4|,
|a3 b1 + a4 b3 , a3 b2 + a4 b4|
=>
|a[0][0] b[0][0] + a[0][1] b[1][0] , a[0][0] b[0][1] + a[0][1] b[1][1]|,
|a[1][0] b[0][0] + a[1][1] b[1][0] , a[1][0] b[0][1] + a[1][1] b[1][1]|
multiMatrix(这一部分说的都是两个矩阵相乘的)其实就是给数组一位一位地去填数,res[0][0] -> res[0][1] -> res[1][0] -> res[1][1]
res[0][0] = a[0][0] b[0][0] + a[0][1] b[1][0],
res[0][1] = a[0][0] b[0][1] + a[0][1] b[1][1],
res[1][0] = a[1][0] b[0][0] + a[1][1] b[1][0],
res[1][1] = a[1][0] b[0][1] + a[1][1] b[1][1].
通过目视法可以得出一个规律:
res[i][j] = a[i][0] b[0][j] + a[i][1] b[1][j]
这是不是就能理解了,这三层 for 循环是怎么计算的?
最内层的 for 循环其实是简化了a[i][0] b[0][j] + a[i][1] b[1][j]这一部分,我们也可以把它写在代码里,不过这样就只能用于二阶矩阵的相乘了。
执行!
不用看了,毫无疑问肯定是可以通过的,不能通过我还会写上来?
但是这运行时间怎么和动态规划一样啊?!
冷静冷静,能学到东西就行。
两种算法的比较
时间复杂度为 log(n) 的算法究竟比时间复杂度为 n 的算法快多少呢?
public static void main(String[] args) {
Solution2 solution2 = new Solution2();
//动态规划算法,时间复杂度为 n
System.out.println("solution2.Fibonacci(i)");
long startTime1 = System.nanoTime();
solution2.Fibonacci(100000000);
long endTime1 = System.nanoTime();
System.out.println((endTime1 - startTime1));
//移动比特位,时间复杂度为 log(n)
System.out.println("solution2.fibonacci(i)");
long startTime2 = System.nanoTime();
solution2.fibonacci(100000000);
long endTime2 = System.nanoTime();
System.out.println((endTime2 - startTime2));
}
运行结果:
solution2.Fibonacci(i)
36458800
solution2.fibonacci(i)
37100
快了 982.7 倍!!
可见 log(n) 是非常快的。
好了,关于斐波那契数列的内容到这里都结束了。
这里是程序员徐小白,【每日算法】是我新开的一个专栏,在这里主要记录我学习算法的日常,也希望我能够坚持每日学习算法,不知道这样的文章风格您是否喜欢,不要吝啬您免费的赞,您的点赞、收藏以及评论都是我下班后坚持更文的动力。
参考资料
# 斐波那契数列系列算法最优复杂度-------O(logN)
二阶常系数线性齐次递推数列_gmqfrostwalker的博客-CSDN博客