快速幂

200 阅读2分钟

快速幂是一种用以快速计算整数或矩阵的幂的一种算法。

整数快速幂

如果要计算一个数x的二次方,可以使用x*x,三次方则是三个x相乘,n次方便是nx相乘,按照这种直观的算法可以写如下代码:

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

显然,这个算法的时间复杂度为O(n),空间复杂度为O(1)。在进行低次幂的运算时,这个方法没有问题,不快,但是还可以接受,但是在计算1000000000次幂,效率还是会很低下。是否有什么方法可以优化一下这个算法: 我们发现,在求x^4时,可以先算const a = x*x, 接着a * a便可以得到结果,这样只是计算了2次,而不需要计算4次。计算17次幂的时候可以 x^17=x^8 * x^8 * x,计算出来一个x^8之后再自身相乘便能计算16次方,再乘以x可得到17次方的结果,这样完全不需要进行17次的计算。根据这个思想,算法如下:

const quickPow = (x, n) => {
    let ans = 1;
    let res = x;
    while (n) {
    	// 最低位为1, ans乘以当前权重 res
        if (n & 1) {
            ans *= res; 
        }
        // 计算下一次循环的权重
        res *= res;
        // 右移一位,准备下一次
        n >>= 1;
    }
    return ans;
};

下面已2^19为例分析一下这个算法: 19的二进制表示为10011,

10011
2^16=655362^8=2562^4=82^2=42^1=2

上述代码中res就是各个位上的权重,并且高位的权限等于低一位的权重的二次方如 2^16 = 2^8 * 2^8 = 256 * 256,上述算法就是对每一位为1的数进行相乘,2^19 = 2^16 * 2^2 * 2

矩阵快速幂

将矩阵快速幂之前,先提一个问题,如何求斐波那契数列第n项? 所谓斐波那契数列就是

f(0) = 0
f(1) = 1 
f(n) = f(n-1) + f(n-2)  n>=2

根据状态转移方程并利用滚桶转移思想可以写出以下代码:

const fibo = (n) => {
    if (n<0) throw new Error('n can not less then 0')
    if (n === 0) return 0;
    if (n === 1) return 1;
    let fn_1 =  1;
    let fn_2 = 0;
    let fn;
    for (let i=2;i<=n;i++) {
        fn = fn_1 + fn_2;
        fn_2 = fn_1;
        fn_1 = fn;
    }
    return fn;
}

显然时间复杂度为O(n), 空间复杂度为O(1), 在n比较大时,效率还是比较低的,可以使用矩阵快速幂对其进行优化。

[f(n)f(n1)]=[1110][f(n1)f(n2)] \left[ \begin{matrix} f(n) \\ f(n-1) \\ \end{matrix} \right] = \left[ \begin{matrix} 1 & 1 \\ 1 & 0 \\ \end{matrix} \right]* \left[ \begin{matrix} f(n-1) \\ f(n-2) \\ \end{matrix} \right]

上面的等式是否成立呢?可以计算一下:

[1110][f(n1)f(n2)]=[f(n1)+f(n2)f(n1)]=[f(n)f(n1)]\left[ \begin{matrix} 1 & 1 \\ 1 & 0 \\ \end{matrix} \right]* \left[ \begin{matrix} f(n-1) \\ f(n-2) \\ \end{matrix} \right] = \left[ \begin{matrix} f(n-1) + f(n-2) \\ f(n-1) \\ \end{matrix} \right]= \left[ \begin{matrix} f(n) \\ f(n-1) \\ \end{matrix} \right]

可以知道上面的等式是成立的,那进行下一步的推导

[1110][f(n1)f(n2)]=[1110]2[f(n2)f(n3)]=[1110]n1[f(1)f(0)]=[1110]n1[10]\left[ \begin{matrix} 1 & 1 \\ 1 & 0 \\ \end{matrix} \right]* \left[ \begin{matrix} f(n-1) \\ f(n-2) \\ \end{matrix} \right] = \left[ \begin{matrix} 1 & 1 \\ 1 & 0 \\ \end{matrix} \right] ^ 2 * \left[ \begin{matrix} f(n-2) \\ f(n-3) \\ \end{matrix} \right]= \left[ \begin{matrix} 1 & 1 \\ 1 & 0 \\ \end{matrix} \right]^{n-1} * \left[ \begin{matrix} f(1) \\ f(0) \\ \end{matrix} \right]= \left[ \begin{matrix} 1 & 1 \\ 1 & 0 \\ \end{matrix} \right]^{n-1} * \left[ \begin{matrix} 1 \\ 0 \\ \end{matrix} \right]

M=[1110] M = \left[ \begin{matrix} 1 & 1 \\ 1 & 0 \\ \end{matrix} \right]

[f(n)f(n1)]=Mn1[10]=[m00m01m10m11][10]=[m00m01]\left[ \begin{matrix} f(n) \\ f(n-1) \\ \end{matrix} \right] = M^{n-1} * \left[ \begin{matrix} 1 \\ 0 \\ \end{matrix} \right]= \left[ \begin{matrix} m_{00} & m_{01} \\ m_{10} & m_{11} \\ \end{matrix} \right] \left[ \begin{matrix} 1 \\ 0 \\ \end{matrix} \right]= \left[ \begin{matrix} m_{00} \\ m_{01} \\ \end{matrix} \right]

所以f(n)为Mn1M^{n-1} 的左上角的值。这个时候可以使用矩阵快速幂了:

const matrixMultiply = (a, b) => {
    const c = [[0,0],[0,0]];
    for(let i=0; i<2;i++) {
        for (let j=0;j<2;j++) {
            c[i][j] = a[i][0] * b[0][j] + a[i][1]*b[1][j];
        }
    }
    return c;
}

const matrixPow = (x, n) => {
    let ans = [[1,0],[0,1]];
    let res = x;
    while(n) {
        if (n&1) {
            ans = matrixMultiply(ans, res);
        }
        res = matrixMultiply(res, res);
        n>>=1;
    }
    return ans;
}

const matrixFibo = (n) => {
    if (n<0) throw new Error('n can not less then 0')
    if (n ==0) return 0;
    if (n === 1) return 1;
    const res = matrixPow([[1,1],[1,0]], n-1);
    return res[0][0];
} 

总结

是怎么想到使用矩阵来解斐波那契数列?
一般情况下,形如 f(n)=i=1maif(ni)f(n)=\sum_{i=1}^{m} a_{i}f(n-i)的状态转移方程都可以转换为这样mmm*m的矩阵的幂求解,转换之后可以使用快速幂获得优化。

[a1a2a3am1000010000100001]\begin{bmatrix} a_1 & a_2 & a_3 & \cdots & a_m \\ 1 & 0 & 0 & \cdots & 0 \\ 0 & 1 & 0 & \cdots & 0 \\ 0 & 0 & 1 & \cdots & 0 \\ \vdots & \vdots& \vdots & \ddots & \vdots\\ 0 & 0 &0 &\cdots&1\\ \end{bmatrix}