快速幂
如何求出 x 的 n 次方?
试想一下,如果要你自己实现一个算法,求出 x 的 n 次方,可以使用语言自带的乘法,你会如何求解?
最简单的方法就是一直做乘法,从 一直乘到
const myPow = (x, n) => {
let ans = 1;
for (let i = 1, i <= n; i++) {
ans = ans * x;
};
return ans;
}
时间复杂度是 O(n)
使用快速幂分治求解
不管别的,我们先假设 n 是一个偶数,例如是 8,根据幂函数求幂的运算法则:
那么
那么就可以写为
myPow(x, 8) = myPow(x * x, 4);
而进一步的
myPow(y, 4) = myPow(y * y, 2);
myPow(z, 2) = z * z;
那如果 n 是一个奇数呢?很简单,根据幂函数求积的运算法则:
我们将 n 转换成偶数就行了,例如 n 为 5 时,;
myPow(x, 5) = x * myPow(x, 5 - 1);
边界情况,如果 n 为 0 呢?一个数的 0 次方,自然是返回1
myPow(x, 0) = 1;
这样我们就可以写出一个更快的解法
const myPow = (x, n) => {
if (n === 0) {
return 1;
}
if (n % 2 === 1) {
return x * myPow(x, n - 1);
} else {
return myPow(x * x, n / 2);
}
}
示意图如下
上面这张图
- 观察其左侧节点,如果不考虑幂运算其计算量是常量级别的:不是 就是 , 同理(注意我们在进行 的运算时,不用考虑,因为在计算时, 的结果在其对应的上一步已经给出)
- 观察其右侧节点,在奇数情况下计算量为常量级别,在偶数情况下每次都是 1/2 减少的 这其实是一种分治的做法,类似排序数组二分查找,他的复杂度是O(logN)
如果 n 为负数呢? 我们知道 x 的 -3 次方其实就是 ,所以当 n < 0 时,我们将 n 取反,将 x 取倒数即可,加上后完整代码如下
const myPow = (x, n) => {
if (n === 0) {
return 1;
}
if (n < 0) {
n = -n;
x = 1 / x; // js 没有 float 和 int 的区别,所以直接做除法
}
if (n % 2 === 1) {
return x * myPow(x, n - 1);
} else {
return myPow(x * x, n / 2);
}
}
矩阵快速幂
矩阵乘法
矩阵乘法就是行列的每一项相乘然后相加
复习一下矩阵的乘法 - 举个例子
快速幂在矩阵的应用
在某些递推的算法中,递推的过程可以用矩阵来表示
直观点说,就是假设我们已知,要求,那么通过分析、递推,可以将表示为
其中代表的是一个矩阵
而求这个矩阵 的 次幂的时候,可以通过快速幂的方式求解,从而降低时间复杂度
所以,矩阵快速幂的应用主要在两点
- 找到递推的矩阵
- 利用快速幂求解
小应用:求第 n 个泰波那契数
泰波那契序列 Tn 定义如下:
T0 = 0, T1 = 1, T2 = 1, 且在 n >= 0 的条件下 Tn+3 = Tn + Tn+1 + Tn+2
给你整数 n,请返回第 n 个泰波那契数 Tn 的值。
求泰波那契数很简单,迭代递归都可以,这次我们尝试用矩阵快速幂的方法来做
根据题意:
我们首先用 替换 ,并转换成咱们熟悉的形式
// i >= 3 时
T[i] = T[i - 1] + T[i - 2] + T[i - 3];
如果我们将它转换成矩阵乘法,那会如何呢
如果我们把也转换成上述的结构呢?
类似的 呢?
好,现在我们开始递推,将向量 递推为向量
则应该按如下矩阵乘法运算
现在,我们找到了递推的矩阵,如果我们将 记为
则可写为:
接下来我们要用快速幂的方法求解
的 次方,那么 应该为多少呢?
我们先从最基础的case开始
进一步的
推两个应该够了,可以看出,最左侧的4 减去 最右侧的2 就是 的次方
所以 n 应该为 i - 2
以下是具体的代码实现
var tribonacci = function (n) {
const getEmptyMatrix = (row, col, defaultValue = 0) => {
// 创建 row x col 的空矩阵,
return Array.from({length: row})
.map(() => {
return Array(col).fill(defaultValue);
});
};
const multiply = (matrixA, matrixB) => {
const matrixC = getEmptyMatrix(
matrixA.length,
matrixB[0].length,
);
for (let i = 0; i < matrixA.length; i++) {
for (let j = 0; j < matrixB[0].length; j++) {
matrixC[i][j] = matrixA[i][0] * matrixB[0][j]
+ matrixA[i][1] * matrixB[1][j]
+ matrixA[i][2] * matrixB[2][j];
}
}
return matrixC;
};
const getPow = (M, x) => {
if (x === 0) {
// 注意这里应该返回单位矩阵,任何矩阵 × 单位矩阵 = 任何矩阵
return [
[1, 0, 0],
[0, 1, 0],
[0, 0, 1],
];
}
if (x % 2 === 1) {
return multiply(M, getPow(M, x - 1));
} else {
return getPow(multiply(M, M), x / 2);
}
};
// === main ===
if (n === 0) {
return 0;
}
if (n <= 2) {
return 1;
}
let initialValues = [
[1], // t2
[1], // t1
[0], // t0
];
let M = [
[1, 1, 1],
[1, 0, 0],
[0, 1, 0],
];
M = getPow(M, n - 2);
const answer = multiply(M, initialValues);
return answer[0][0];
};
矩阵快速幂适用于哪些情况?
各位可以看出,其实这是动态规划的一种:我们已知了一个初始态,并且得出了状态转移方程。限制点在于,这个状态转移方程可用矩阵表示(这意味着运算的过程不算太复杂)
阅读完后可以考虑进阶的练习: 1220. 统计元音字母序列的数目
写在最后
快速幂还有一种迭代的算法,而非递归,后续考虑更新