• 快速幂及其应用


      计算指数函数ap(p>0)在p点的值,朴素算法将a累乘p次得出结果,时间复杂度为O(p),当指数p很大时(>1e7)算法在机器上需要花大量运行时间。

      改进朴素算法,采用分治策略:当p为偶数时,将ap分解为(ap/2)*(ap/2)两因子,而ap/2又可继续做同样的划分,直到降为容易计算的低次幂;当p为奇数时,从式中提出因子a,剩下的ap-1就化为了偶指数形式。

      形式化描述为:

    egin{equation}
    a(p) = left{
    egin{array}{**l**}
    a(frac{p}{2}) cdot a(frac{p}{2}) & p=2k, k in N^+ \
    a(frac{p-1}{2}) cdot a(frac{p-1}{2}) cdot a & p=2k+1, k in N \
    1 & p=0
    end{array}
    ight.
    end{equation}

      设T(p)为算法在规模p下花费的时间,则根据递推方程有:

    egin{equation}
    T(p) = left{
    egin{array}{**l**}
    T(lfloor frac{p}{2} floor) + O(1) & p>0 \
    O(1) & p=0
    end{array}
    ight.
    end{equation}

      根据Master Theory可得$ T(p) = O(log{p}) $

      可见时间复杂度有了很大改善。直观地体会一下,例如指数p=18,446,744,073,709,551,616(264)时,只需执行64次乘法。(这里不考虑结果的溢出)

      如何实现这个算法呢?朴素的做法是递归:

    typedef long long ll;
    
    ll fspow(ll a, ll p) {
        if(p==0)
            return 1;
        if(p&1) {
            ll t = fspow(a, p-1);
            return t*a;
        } else {
            ll t = fspow(a, p/2);
            return t*t;
        }
    }

     

    两行代码的非递归实现

      递归算法缺陷在于空间复杂度为O(log p),非递归算法避免了额外的存储空间。

    1 for (s=1; p;p/=2,a=a*a)
    2         if (p&1) s=s*a;

      其中,

    输入变量:a为底数,p为指数

    输出变量:s=ap

      再来看原理。思路仍是分治,这与我们之前的讨论是一致的,但不再递归地求解。

      对于偶指数,每次划分指数折半(1/2),此时要确保等价,底数必须平方,即$ a^p = (a^2)^{frac{p}{2}} $。而指数p/2又可继续分为p/4 p/8……1,同时底数随之平方。当指数降为1时,底数即为幂的值。当指数不是偶数时,先拆出一个底数,则剩下的幂就是偶次幂,再按如上方法处理即可。

      注意如下细节:

        1. 当p为奇数时,$ lfloor frac{p}{2} floor = frac{p-1}{2} $。

        2. 利用位运算避免%运算。

    具体应用

    1. RSA

      幂运算在许多场合都有着重要的运用。例如RSA非对称加密算法中,明文通过如下公式进行加密:

    Ci = Mie mod n

        其中Ci为密文,Mi为与明文有关的数值,(e, n)为公钥对。

      而密文通过如下公式解密:

    Mi = Cid mod n

      其中Mi为与明文有关的数值,(d, n)为私钥对。

     

      可以看到,无论是加密还是解密,都要用到幂运算与模运算。这就与我们上文所述的快速幂运算很有关联。

     

      不止RSA,在其它运用中也常常出现类型的形式,它们归结下来如下面这个式子:

    ap mod k

      由于对幂取模,最终结果并不会大于等于k。再看看我们的快速幂算法,中间结果很可能远大于k,如果中间结果超出基本数据类型的范围,还需要通过高精度模拟运算。我们很快发现,当k可通过基本数据类型表示时,似乎没有必要采用高精度算法,上文的快速幂算法还可以改进。

      随时取模性质指出,(a*b) mod k = (a mod k)*(b mod k) mod k。即两个数的乘积再取模,等于两个数分别取模再相乘,最后取模。利用这个性质,我们可以将ap mod k等价变形为(a mod k)p mod k,同时每次做乘法时,都先利用该性质缩小乘数,再相乘。

      上述代码修改为

    1 a %= k;
    2 for (s=1; p;p/=2,a=(a*a)%k)
    3          if (p&1) s=(s*a)%k;

      其中a,p,k,s的含义与上文一致。

      这便是快速幂模k算法的非递归实现。

    2. 矩阵幂加速

    快速Fibonacci计算

      Fibonacci数列的递推定义如下:

      为了将递推式用线性代数的形式表达出来,设待定系数a,b,c,d。当n>=2时,建立如下等式:

      解出各未知量,得$ a=1,b=1,c=1,d=0 $,即递推式等价于:

      注意上式必须满足n>=2。结合n<2的基准情况,可以得到任意$ f_n $、$ f_{n-1} $的表达式如下:

      即,只需计算出左矩阵的n次幂,即可求出Fibonacci的第n-1和第n项。若采用快速幂算法计算矩阵幂,我们会惊讶地发现求出该数列第n项只需O(log n)的时间复杂度。

      如何实现矩阵乘法?只需封装矩阵ADT(抽象数据类型)。利用语言特性重载*=运算符。

    typedef long long ll;
    
    class mat{
        ll a,b,c,d;
        inline void operator*=(const mat &m) {
            mat n;
            n.a=a*m.a+b*m.c;
            n.b=a*m.b+b*m.d;
            n.c=c*m.a+d*m.c;
            n.d=c*m.b+d*m.d; 
            *this=n;
        }
    };
    
    ll fib(int n) {
        mat s={1,0,0,1};
        mat p={1,1,1,0};
        
        for(; n; n/=2,p*=p) {
            if(n&1) s*=p;
        }
        return s.a;
    }

      实际应用中由于数值较大,往往需要对结果取模。可利用随时取模性质,在计算矩阵乘法时对中间结果取模。

  • 相关阅读:
    第四周助教小结
    java课程总结
    第十四周总结
    第十三周实验报告
    第十二周课程报告
    第十一周课程总结
    第十周课程总结
    第九周课程总结&实验报告(七)
    第八周课程总结&实验报告(六)
    第七周课程总结&实验报告(五)
  • 原文地址:https://www.cnblogs.com/cassuto/p/11809300.html
Copyright © 2020-2023  润新知