从快速幂到矩阵快速幂

209 阅读3分钟

快速幂

如何求出 x 的 n 次方?

leetcode 51 实现一个pow算法

试想一下,如果要你自己实现一个算法,求出 x 的 n 次方,可以使用语言自带的乘法,你会如何求解?

最简单的方法就是一直做乘法,从 xx 一直乘到 xnx^n

const myPow = (x, n) => {
    let ans = 1;
    for (let i = 1, i <= n; i++) {
        ans = ans * x;
    };
    return ans;
}

时间复杂度是 O(n)

使用快速幂分治求解

不管别的,我们先假设 n 是一个偶数,例如是 8,根据幂函数求幂的运算法则: (xa)b=x(ab)(x ^ a) ^ b = x ^{(ab)}

那么 (x2)4=x8(x ^ 2) ^ 4 = x ^ 8

那么就可以写为

myPow(x, 8) = myPow(x * x, 4);

而进一步的

myPow(y, 4) = myPow(y * y, 2);
myPow(z, 2) = z * z;

那如果 n 是一个奇数呢?很简单,根据幂函数求积的运算法则:x(a+b)=(xa)(xb)x^{(a + b)} = (x ^ a) * (x ^ b)

我们将 n 转换成偶数就行了,例如 n 为 5 时,x5=(x4)(x1)x ^ 5 = (x ^ 4) * (x ^ 1);

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);
    }
}

示意图如下

image.png

上面这张图

  • 观察其左侧节点,如果不考虑幂运算其计算量是常量级别的:不是 xx 就是 xxx * x, yy zz 同理(注意我们在进行yy zz的运算时,不用考虑xx,因为在计算时, yy zz 的结果在其对应的上一步已经给出)
  • 观察其右侧节点,在奇数情况下计算量为常量级别,在偶数情况下每次都是 1/2 减少的 这其实是一种分治的做法,类似排序数组二分查找,他的复杂度是O(logN)

如果 n 为负数呢? 我们知道 x 的 -3 次方其实就是 1/(x3)1/(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);
    }
}

矩阵快速幂

矩阵乘法

An×k×Bk×m=Cn×mA_{n \times k} \times B_{k \times m} = C_{n \times m}

矩阵乘法就是行列的每一项相乘然后相加

复习一下矩阵的乘法 - 举个例子

[a11 a12a21 a22][b11 b12b21 b22]=[a11×b11+a12×b21a11×b12+a12×b22a21×b11+a22×b21a21×b12+a22×b22]{\left[\begin{matrix} a_{11} \ a_{12} \\ a_{21} \ a_{22} \end{matrix}\right]}*{\left[\begin{matrix} b_{11} \ b_{12} \\ b_{21} \ b_{22} \end{matrix}\right]} = {\left[\begin{matrix} a_{11} \times b_{11} + a_{12} \times b_{21} \quad a_{11} \times b_{12} + a_{12} \times b_{22} \\ a_{21} \times b_{11} + a_{22} \times b_{21} \quad a_{21} \times b_{12} + a_{22} \times b_{22} \end{matrix}\right]}

快速幂在矩阵的应用

在某些递推的算法中,递推的过程可以用矩阵来表示

直观点说,就是假设我们已知A0A_0,要求AnA_n,那么通过分析、递推,可以将AnA_n表示为

An=Mm×A0A_n = M^m \times A_0

其中MM代表的是一个矩阵

而求这个矩阵 MMnn 次幂的时候,可以通过快速幂的方式求解,从而降低时间复杂度

所以,矩阵快速幂的应用主要在两点

  1. 找到递推的矩阵 MM
  2. 利用快速幂求解 MnM^n

小应用:求第 n 个泰波那契数

leetcode 1137. 第 N 个泰波那契数

泰波那契序列 Tn 定义如下: 

T0 = 0, T1 = 1, T2 = 1, 且在 n >= 0 的条件下 Tn+3 = Tn + Tn+1 + Tn+2

给你整数 n,请返回第 n 个泰波那契数 Tn 的值。

求泰波那契数很简单,迭代递归都可以,这次我们尝试用矩阵快速幂的方法来做

根据题意:Tn+3=Tn+Tn+1+Tn+2T_{n+3} = T_{n} + T_{n+1} + T_{n+2}

我们首先用 ii 替换 nn,并转换成咱们熟悉的形式

// i >= 3 时
T[i] = T[i - 1] + T[i - 2] + T[i - 3];

如果我们将它转换成矩阵乘法,那会如何呢

T[i]=[1 1 1][T[i1]T[i2]T[i3]]{T[i]} = {\left[\begin{matrix} 1 \ 1 \ 1 \end{matrix}\right]} * {\left[\begin{matrix} T[i - 1] \\ T[i - 2] \\ T[i - 3] \end{matrix}\right]}

如果我们把T[i1]T[i-1]也转换成上述的结构呢?

T[i1]=[1 0 0][T[i1]T[i2]T[i3]]{T[i-1]} = {\left[\begin{matrix} 1 \ 0 \ 0 \end{matrix}\right]} * {\left[\begin{matrix} T[i - 1] \\ T[i - 2] \\ T[i - 3] \end{matrix}\right]}

类似的 T[i2]T[i-2] 呢?

T[i2]=[0 1 0][T[i1]T[i2]T[i3]]{T[i - 2]} = {\left[\begin{matrix} 0 \ 1 \ 0 \end{matrix}\right]} * {\left[\begin{matrix} T[i - 1] \\ T[i - 2] \\ T[i - 3] \end{matrix}\right]}

好,现在我们开始递推,将向量 [T[i1]T[i2]T[i3]]{\left[\begin{matrix} T[i - 1] \\ T[i - 2] \\ T[i - 3] \end{matrix}\right]} 递推为向量 [T[i]T[i1]T[i2]]{\left[\begin{matrix} T[i] \\ T[i - 1] \\ T[i - 2] \end{matrix}\right]}

则应该按如下矩阵乘法运算

[T[i]T[i1]T[i2]]=[1 1 11 0 00 1 0][T[i1]T[i2]T[i3]]{\left[\begin{matrix} T[i] \\ T[i - 1] \\ T[i - 2] \end{matrix}\right]} = {\left[\begin{matrix} 1 \ 1 \ 1 \\ 1 \ 0 \ 0 \\ 0 \ 1 \ 0 \end{matrix}\right]}*{\left[\begin{matrix} T[i - 1] \\ T[i - 2] \\ T[i - 3] \end{matrix}\right]}

现在,我们找到了递推的矩阵,如果我们将 [1 1 11 0 00 1 0]{\left[\begin{matrix} 1 \ 1 \ 1 \\ 1 \ 0 \ 0 \\ 0 \ 1 \ 0 \end{matrix}\right]} 记为 MM

则可写为:

[T[i]T[i1]T[i2]]=Mn[T[i1]T[i2]T[i3]]{\left[\begin{matrix} T[i] \\ T[i - 1] \\ T[i - 2] \end{matrix}\right]} = {M^n}*{\left[\begin{matrix} T[i - 1] \\ T[i - 2] \\ T[i - 3] \end{matrix}\right]}

接下来我们要用快速幂的方法求解MnM^n

MMnn 次方,那么 nn 应该为多少呢?

我们先从最基础的case开始

[T[3]T[2]T[1]]=M1[T[2]T[1]T[0]]{\left[\begin{matrix} T[3] \\ T[2] \\ T[1] \end{matrix}\right]} = {M^1}*{\left[\begin{matrix} T[2] \\ T[1] \\ T[0] \end{matrix}\right]}

进一步的

[T[4]T[3]T[2]]=M1[T[3]T[2]T[1]]=MM[T[2]T[1]T[0]]=M2[T[2]T[1]T[0]]{\left[\begin{matrix} T[4] \\ T[3] \\ T[2] \end{matrix}\right]} = {M^1}*{\left[\begin{matrix} T[3] \\ T[2] \\ T[1] \end{matrix}\right]} = {M * M}*{\left[\begin{matrix} T[2] \\ T[1] \\ T[0] \end{matrix}\right]} = {M^2}*{\left[\begin{matrix} T[2] \\ T[1] \\ T[0] \end{matrix}\right]}

推两个应该够了,可以看出,最左侧的4 减去 最右侧的2 就是 MM 的次方

所以 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. 统计元音字母序列的数目

写在最后

快速幂还有一种迭代的算法,而非递归,后续考虑更新

参考链接

泰波那契数【宫水三叶】的题解

泰波那契数 力扣官方的题解