LeetCode 上有几道斐波那契数相关的问题,这类问题一般使用动态规划求解。
后来看了 《程序员代码面试指南:IT名企算法与数据结构题目最优解》,才知道这类问题使用快速幂求解矩阵乘积,可以在
为了方便后面复习,把 LeetCode 上斐波那契数和快速幂相关问题统一整理起来。
相关问题
问题描述:
我们可以使用动态规划求解斐波那契数,求解过程如下:
1、状态:给定下标
2、数组:定义长度是
3、base case:斐波那契数列的边界,$dp[0] = 0、dp[1] = 1$
4、状态转移方程: $$ \begin{aligned} dp[n] &= \begin{cases} dp[n-1] + dp[n-2], \quad n \ge 2 \ n, \quad n \le 1 \ \end{cases} \end{aligned} $$
斐波那契数列的定义本身就包含了动态规划的“状态、数组、base case、状态转移方程”,并且,$F(n)$ 只和
代码实现如下:
class Solution {
public int fib(int n) {
final int MOD = (int) 1e9 + 7;
if (n <= 1) {
return n;
}
int f1 = 0, f2 = 1;
for (int i = 2; i <= n; i++) {
int f3 = (f1 + f2) % MOD;
f1 = f2;
f2 = f3;
}
return f2;
}
}
问题描述:
该问题的求解思路与509. 斐波那契数相似,区别在于
代码实现如下:
class Solution {
public int tribonacci(int n) {
if (n == 0) {
return 0;
} else if (n <= 2) {
return 1;
}
int num1 = 0, num2 = 1, num3 = 1;
for (int i = 3; i <= n; i++) {
int tmp = (num1 + num2 + num3);
num1 = num2;
num2 = num3;
num3 = tmp;
}
return num3;
}
}
问题描述:
这一题使用动态规划求解时,状态转移方程和斐波那契数相同。求解过程如下:
1、状态:给定台阶数
2、数组:定义长度是
3、base case:每次可以爬1个台阶或2个台阶,得到边界条件,$dp[1] = 1、dp[2] = 2$
4、状态转移方程:
要到达台阶
动态规划的求解过程与前面两个问题相同。
代码实现如下:
class Solution {
public int climbStairs(int n) {
if (n <= 2) {
return n;
}
int f1 = 1, f2 = 2, f3 = 0;
for (int i = 3; i <= n; i++) {
f3 = f1 + f2;
f1 = f2;
f2 = f3;
}
return f3;
}
}
前面的3个问题,都是简单的序列型动态规划题,使用上面介绍的动态规划方法,可以在
那有没有更快的求解方法?能否在
快速幂的算法描述,来自参考阅读:快速幂
二进制求幂
计算
不过,根据幂运算规则,我们知道:
$$
a^{b+c} = a^b \cdot a^c,\quad a^{2b} = a^b \cdot a^b = (a^b)^2
$$
根据该运算规则,可以使用快速幂算法来求解。快速幂的基本思想是:把指数
把整数
在上面的公式中,各个乘积项
我们通过一个例子($a = 3,n = 13$)来演示上述的计算过程。
我们将
根据前面的算法描述,我们可以快速的计算出 3 的各项次幂的值,如下所示:
$$
\begin{aligned} \&
3^1 = 3 \&
3^2 = (3^1)^2 = 9 \&
3^4 = (3^2)^2 = 81 \&
3^8 = (3^4)^2 = 6561
\end{aligned}
$$
要计算出
这个算法的复杂度是
- 计算了
$\Theta(\log n)$ 个 2 的次幂 - 花费
$\Theta(\log n)$ 的时间选择二进制位为 1 的幂来相乘
public int binaryExpo(int a, int n) {
int res = 1;
while (n > 0) {
// 当前位的值等于1,要乘上该位上的值
if ((n & 1) == 1) {
res = res * a;
}
// 每移动一位,要变成前一位值的平方
a = a * a;
n = n >> 1;
}
return res;
}
LeetCode 上计算幂函数的问题,可以使用上面介绍的快速幂求解。代码如下:
class Solution {
public double myPow(double x, int n) {
// n 会超出位数
long N = n;
N = Math.abs(N);
double res = binaryExpo(x, N);
if (n < 0) {
return 1 / res;
} else {
return res;
}
}
public double binaryExpo(double a, long n) {
double res = 1;
while (n > 0) {
// 当前位的值等于1,要乘上该位上的值
if ((n & 1) == 1) {
res = res * a;
}
// 每移动一位,要变成前一位值的平方
a = a * a;
n = n >> 1;
}
return res;
}
}
下面的内容,来自参考阅读:《程序员代码面试指南:IT名企算法与数据结构题目最优解》
快速幂怎么用来求解斐波那契数?
斐波那契数的转移方程:$F(n) = F(n-1) + F(n-2)$,可以写作矩阵乘法的形式:
$$ \begin{aligned} \left[F(n),F(n-1)\right]
= \left[F(n-1),F(n-2)\right] \times {\left| \begin{matrix} 1 & 1 \ 1 & 0 \end{matrix} \right|} \&
\end{aligned} $$
斐波那契数的计算公式可以表示为:
$$ \begin{aligned} \left[F(n),F(n-1)\right] \&
= \left[F(n-1),F(n-2)\right] \times {\left| \begin{matrix} 1 & 1 \ 1 & 0 \end{matrix} \right|} \&
= \left[F(n-2),F(n-3)\right] \times {\left| \begin{matrix} 1 & 1 \ 1 & 0 \end{matrix} \right|}^2 \&
= \cdots \&
= \left[F(2),F(1)\right] \times {\left| \begin{matrix} 1 & 1 \ 1 & 0 \end{matrix} \right|}^{n-2} \&
\end{aligned} $$
需要计算矩阵的
用于计算矩阵的快速幂代码如下:
/**
* 矩阵的快速幂
*
* @param mat 矩阵
* @param n 次幂
*/
public int[][] binaryExpo(int[][] mat, int n) {
int[][] res = new int[mat.length][mat[0].length];
for (int i = 0; i < res.length; i++) {
res[i][i] = 1;
}
while (n > 0) {
// 当前位的值等于1,要乘上该位上的值
if ((n & 1) == 1) {
res = mulMat(res, mat);
}
// 每移动一位,要变成前一位值的平方
mat = mulMat(mat, mat);
n = n >> 1;
}
return res;
}
/**
* 计算两个矩阵相乘:mat1 * mat2
*
* @param mat1 矩阵1
* @param mat2 矩阵2
*/
public int[][] mulMat(int[][] mat1, int[][] mat2) {
int m = mat1.length, n = mat1[0].length, l = mat2[0].length;
int[][] res = new int[m][l];
for (int i = 0; i < m; i++) {
for (int j = 0; j < l; j++) {
for (int k = 0; k < n; k++) {
res[i][j] += mat1[i][k] * mat2[k][j];
}
}
}
return res;
}
有了矩阵的快速幂计算方法,就可以使用快速幂来计算前面的问题。
按照前面的分析,前面斐波那契数、泰波那契数、爬楼梯问题,都可以使用快速幂来计算,它们的区别只在于初始值不同、状态转移矩阵不同。
代码实现如下。
class Solution {
public int fib(int n) {
if (n < 1) {
return 0;
}
if (n <= 2) {
return 1;
}
int[][] res = binaryExpo(new int[][]{{1, 1}, {1, 0}}, n - 2);
return res[0][0] + res[1][0];
}
public int[][] binaryExpo(int[][] mat, int n) {
int[][] res = new int[mat.length][mat[0].length];
for (int i = 0; i < res.length; i++) {
res[i][i] = 1;
}
while (n > 0) {
// 当前位的值等于1,要乘上该位上的值
if ((n & 1) == 1) {
res = mulMat(res, mat);
}
// 每移动一位,要变成前一位值的平方
mat = mulMat(mat, mat);
n = n >> 1;
}
return res;
}
public int[][] mulMat(int[][] mat1, int[][] mat2) {
int m = mat1.length, n = mat1[0].length, l = mat2[0].length;
int[][] res = new int[m][l];
for (int i = 0; i < m; i++) {
for (int j = 0; j < l; j++) {
for (int k = 0; k < n; k++) {
res[i][j] += mat1[i][k] * mat2[k][j];
}
}
}
return res;
}
}
class Solution {
public int tribonacci(int n) {
if (n <= 1) {
return n;
}
if (n <= 3) {
return n - 1;
}
int[][] res = binaryExpo(new int[][]{{1, 1, 0}, {1, 0, 1}, {1, 0, 0}}, n - 3);
return 2 * res[0][0] + res[1][0] + res[2][0];
}
public int[][] binaryExpo(int[][] mat, int n) {
int[][] res = new int[mat.length][mat[0].length];
for (int i = 0; i < res.length; i++) {
res[i][i] = 1;
}
while (n > 0) {
// 当前位的值等于1,要乘上该位上的值
if ((n & 1) == 1) {
res = mulMat(res, mat);
}
// 每移动一位,要变成前一位值的平方
mat = mulMat(mat, mat);
n = n >> 1;
}
return res;
}
public int[][] mulMat(int[][] mat1, int[][] mat2) {
int m = mat1.length, n = mat1[0].length, l = mat2[0].length;
int[][] res = new int[m][l];
for (int i = 0; i < m; i++) {
for (int j = 0; j < l; j++) {
for (int k = 0; k < n; k++) {
res[i][j] += mat1[i][k] * mat2[k][j];
}
}
}
return res;
}
}
class Solution {
public int climbStairs(int n) {
if (n <= 2) {
return n;
}
int[][] res = binaryExpo(new int[][]{{1, 1}, {1, 0}}, n - 2);
return 2 * res[0][0] + res[1][0];
}
/**
* 矩阵的快速幂
*
* @param mat 矩阵
* @param n 次幂
*/
public int[][] binaryExpo(int[][] mat, int n) {
int[][] res = new int[mat.length][mat[0].length];
for (int i = 0; i < res.length; i++) {
res[i][i] = 1;
}
while (n > 0) {
// 当前位的值等于1,要乘上该位上的值
if ((n & 1) == 1) {
res = mulMat(res, mat);
}
// 每移动一位,要变成前一位值的平方
mat = mulMat(mat, mat);
n = n >> 1;
}
return res;
}
/**
* 计算两个矩阵相乘:mat1 * mat2
*
* @param mat1 矩阵1
* @param mat2 矩阵2
*/
public int[][] mulMat(int[][] mat1, int[][] mat2) {
int m = mat1.length, n = mat1[0].length, l = mat2[0].length;
int[][] res = new int[m][l];
for (int i = 0; i < m; i++) {
for (int j = 0; j < l; j++) {
for (int k = 0; k < n; k++) {
res[i][j] += mat1[i][k] * mat2[k][j];
}
}
}
return res;
}
}
与传统的动态规划方法相比,快速幂能在更快地时间复杂度内求解问题。
但是在代码实现上,动态规划更加简单,快速幂比较较为复杂。
问题描述:
解题思路与70. 爬楼梯相似,只不过在状态转移时,要加上当前台阶上的花费,并取最小值。
class Solution {
public int minCostClimbingStairs(int[] cost) {
int n = cost.length;
if (n == 1) {
return cost[0];
}
if (n == 2) {
return Math.min(cost[0], cost[1]);
}
int f1 = cost[0], f2 = cost[1], res = 0;
for (int i = 2; i <= n; i++) {
int cs = 0;
if (i < n) {
cs = cost[i];
}
res = Math.min(f1 + cs, f2 + cs);
f1 = f2;
f2 = res;
}
return res;
}
}
1、快速幂
2、矩阵