快速幂算法

问题引入

考虑如下问题:计算a的b次方

当然这个结果可能会很大,哪怕是long long类型也存不下。所以我们对计算结果做一个约束,比如说后3位,即求a的b次方的后3位。

正常人的思路应该是一个for循环累乘,用代码展现就是下面这个样子:

int power(int a, int b)
{
    //利用循环,这是最基础的写法,最慢
    int sum = 1;
    for (int i = 1; i <= b; i++)
        sum = sum * a % mod;
    return sum;
}

其中,mod是模数,可以根据需求取后几位。显然,这个方法的时间复杂度为 O ( b ) O(b) O(b)。当b很大的时候这个算法的性能就略显不足了。

快速幂基本原理

怎么优化呢?这就要用到二分的思想了: a b a^{b} ab可以这样计算 ( a 2 ) b / 2 (a^{2})^{b/2} (a2)b/2,这样一来b的值就减小了一半,节约了一半的时间。如果底数继续平方,指数继续减半,这样就能把时间复杂度降到 O ( log ⁡ 2 b ) O(\log_{2}{b} ) O(log2b)。当然这里有个情况,b不总是2的倍数。当b不是2的倍数的时候,特殊处理一下,单独拿出一个a乘到结果上就好了。代码如下:

int power1(int a, int b)
{
    //快速幂,基本原理
    int sum = 1;
    while (b > 0)
    {
        if (b % 2 == 0)
        {
            //指数为偶数,底数平方一下,指数减半
            a = a * a % mod;
            b /= 2;
        }
        else
        {
            //指数为奇数,拿出一个底数,剩下的就是偶数次了,然后和上面同样操作
            b--; //指数减一,变成偶数
            sum = sum * a % mod;
            a = a * a % mod;
            b /= 2;
        }
    }
    return sum;
}

这就是快速幂的基本原理了,不过上面这个写法还不够简洁,还不够快。观察上述代码可以发现,a = a * a % mod; b /= 2;这两行代码出现了两次,这是完全没有必要的;还有判断奇偶、除以2可以用位运算来代替。优化后的代码如下:

int power2(int a, int b)
{
    //终极写法,最快
    int sum = 1;
    while (b > 0)
    {
        if (b & 1) //等同于if(b % 2 == 1)
            sum = sum * a % mod;
        b >>= 1; //等同于b/=2
        a = a * a % mod;
    }
    return sum;
}

矩阵快速幂

矩阵快速幂和上面的思想是一样的,只不过上面都是整数的运算,这里进行的是矩阵的运算。矩阵乘法怎么运算参考线性代数。洛谷上有道矩阵快速幂的模板题,可以参考一下:https://www.luogu.com.cn/problem/P3390

下面给出矩阵快速幂的代码:

#include 
#include 

using namespace std;

int n;
const long long mod = 1e9 + 7;
struct arr
{
    long long num[105][105];

    arr()
    {
        memset(num, 0, sizeof(num));
    }
};

arr multiply(arr a, arr b)
{
    // 计算方阵a乘方阵b
    arr res;
    for (int i = 0; i < n; i++)
        for (int j = 0; j < n; j++)
            for (int k = 0; k < n; k++)
                res.num[i][j] = (res.num[i][j] + a.num[i][k] * b.num[k][j]) % mod;
    return res;
}

arr power(arr a, long long k)
{
    arr res;
    for (int i = 0; i < n; i++) // 构造单位矩阵
        res.num[i][i] = 1;
    while (k > 0)
    {
        if (k & 1)
            res = multiply(res, a);
        k >>= 1;
        a = multiply(a, a);
    }
    return res;
}

int main()
{
    long long k;
    arr a;
    cin >> n >> k;
    for (int i = 0; i < n; i++)
        for (int j = 0; j < n; j++)
            cin >> a.num[i][j];

    arr res = power(a, k);
    for (int i = 0; i < n; i++)
    {
        for (int j = 0; j < n; j++)
            cout << res.num[i][j] << " ";
        cout << endl;
    }
    return 0;
}

斐波那契数列

矩阵快速幂的一个简单应用就是斐波那契数列。斐波那契数列就是形如:1,1,2,3,5,8……的数列。它的数学描述如下:
F n = { 1 , n ≤ 2 F n − 1 + F n − 2 , n ≥ 3 F_{n}=\left\{\begin{matrix} 1&,n\le 2 \\ F_{n-1}+F_{n-2} &,n\ge 3 \end{matrix}\right. Fn={1Fn1+Fn2,n2,n3
斐波那契数列的增长是很快的,所以我们经常求模来限制结果的长度,例如 F n m o d    1 0 9 + 7 F_{n} \mod 10^{9}+7 Fnmod109+7

求斐波那契数列第n项的一个朴素思路就是按照递推公式循环递推下去,其时间复杂度显然为 O ( n ) O(n) O(n)。然而如果能够利用快速幂的思想优化的话,就能够降到 O ( log ⁡ 2 n ) O(\log_{2}{n} ) O(log2n)

考虑斐波那契数列的递推公式为 F n = F n − 1 + F n − 2 F_{n}=F_{n-1}+F_{n-2} Fn=Fn1+Fn2,将其矩阵化的话每一项应该是像 [ F n − 1 F n − 2 ] \begin{bmatrix} F_{n-1}& F_{n-2} \end{bmatrix} [Fn1Fn2]这样的。那么,等式左边也应该变成这样 [ F n F n − 1 ] \begin{bmatrix} F_{n} & F_{n-1} \end{bmatrix} [FnFn1]。问题就变成了找到一个矩阵 X X X使得 [ F n F n − 1 ] = [ F n − 1 F n − 2 ] ∗ X \begin{bmatrix} F_{n} &F_{n-1} \end{bmatrix}=\begin{bmatrix} F_{n-1} &F_{n-2} \end{bmatrix}*X [FnFn1]=[Fn1Fn2]X成立。显然矩阵 X X X应该是两行两列的,即
[ F n F n − 1 ] = [ F n − 1 F n − 2 ] ∗ [ x 1 x 2 x 3 x 4 ] \begin{bmatrix} F_{n} &F_{n-1} \end{bmatrix}=\begin{bmatrix} F_{n-1} &F_{n-2} \end{bmatrix}*\begin{bmatrix} x_{1}&x_{2} \\ x_{3}&x_{4} \end{bmatrix} [FnFn1]=[Fn1Fn2][x1x3x2x4]
根据矩阵运算规则, { F n = x 1 F n − 1 + x 3 F n − 2 F n − 1 = x 2 F n − 1 + x 4 F n − 2 \left\{\begin{matrix} F_{n}=x_{1}F_{n-1}+x_{3}F_{n-2} \\ F_{n-1}=x_{2}F_{n-1}+x_{4}F_{n-2} \end{matrix}\right. {Fn=x1Fn1+x3Fn2Fn1=x2Fn1+x4Fn2。显然, x 1 , x 2 , x 3 , x 4 x_{1},x_{2},x_{3},x_{4} x1,x2,x3,x4的值都能求出来。所以,矩阵 X = [ 1 1 1 0 ] X=\begin{bmatrix} 1& 1\\ 1&0 \end{bmatrix} X=[1110]

根据矩阵化的递推公式 [ F n F n − 1 ] = [ F n − 1 F n − 2 ] ∗ [ 1 1 1 0 ] \begin{bmatrix} F_{n} &F_{n-1} \end{bmatrix}=\begin{bmatrix} F_{n-1} &F_{n-2} \end{bmatrix}*\begin{bmatrix} 1&1 \\ 1&0 \end{bmatrix} [FnFn1]=[Fn1Fn2][1110],将等式右边不断展开, [ F n F n − 1 ] = [ F n − 2 F n − 3 ] ∗ [ 1 1 1 0 ] 2 = … … \begin{bmatrix} F_{n} &F_{n-1} \end{bmatrix}=\begin{bmatrix} F_{n-2} &F_{n-3} \end{bmatrix}*{\begin{bmatrix} 1&1 \\ 1&0 \end{bmatrix}}^{2}=…… [FnFn1]=[Fn2Fn3][1110]2=……
直到 [ F n F n − 1 ] = [ F 2 F 1 ] ∗ [ 1 1 1 0 ] n − 2 \begin{bmatrix} F_{n} &F_{n-1} \end{bmatrix}=\begin{bmatrix} F_{2} &F_{1} \end{bmatrix}*{\begin{bmatrix} 1&1 \\ 1&0 \end{bmatrix}}^{n-2} [FnFn1]=[F2F1][1110]n2。此时 F 2 F_{2} F2 F 1 F_{1} F1都是已知的,都为1,所以问题就变成了求右边矩阵的n-2次方。这就是上面矩阵快速幂的内容。

斐波那契数列在洛谷上也有模板题:https://www.luogu.com.cn/problem/P1962
AC代码如下:

#include 
#include 

using namespace std;

int n = 2;                     // 方阵阶数
const long long mod = 1e9 + 7; // 模数
struct arr
{
    long long num[105][105];

    arr()
    {
        memset(num, 0, sizeof(num));
    }
};

arr multiply(arr a, arr b)
{
    // 计算方阵a乘方阵b
    arr res;
    for (int i = 0; i < n; i++)
        for (int j = 0; j < n; j++)
            for (int k = 0; k < n; k++)
                res.num[i][j] = (res.num[i][j] + a.num[i][k] * b.num[k][j]) % mod;
    return res;
}

arr power(arr a, long long k)
{
    arr res;
    for (int i = 0; i < n; i++) // 构造单位矩阵
        res.num[i][i] = 1;
    while (k > 0)
    {
        if (k & 1)
            res = multiply(res, a);
        k >>= 1;
        a = multiply(a, a);
    }
    return res;
}

int main()
{
    arr a;
    long long x; // 第x项
    cin >> x;
    if (x < 3)
    {
        cout << 1 << endl;
        return 0;
    }
    // 递推矩阵赋值
    a.num[0][0] = 1;
    a.num[0][1] = 1;
    a.num[1][0] = 1;
    a.num[1][1] = 0;
    arr res = power(a, x - 2);

    cout << (res.num[0][0] + res.num[1][0]) % mod << endl;
    return 0;
}

你可能感兴趣的:(算法,算法,c++)