浅谈快速幂/快速乘

浅谈快速幂

这篇随笔简单讲解一下数学问题种快速幂的实现原理及实现。


快速幂的用途

顾名思义,快速幂就是很快速的幂运算,试想当你面对一个问题:求\(a^b\)的时候,你的第一反应是开\(long long\)然后用\(for\)循环一点一点求。那么你就已经会了幂运算的\(O(b)\)算法。按常理来讲,这样的算法已经够用了,但是遇到一些卡时间的题目的时候还是会\(T\),于是快速幂应运而生。简单地说,快速幂就是一种复杂度为\(O(logb)\)的求幂运算的算法。


快速幂的实现原理

因为快速幂的时间复杂度是\(O(logb)\)的,所以我们自然而然地想到了二进制及位运算。这是显然的,我们知道,一个整数可以被拆分成若干个\(2^k\)的和。那么类比一下,对于一个幂运算问题\(a^b\),我们可以把\(b\)二进制分解,成为若干个\(2^k\)的和,那么对应下来就是这些和的幂的乘积。

举个例子:

求解问题:\(3^{42}\)

第一步,将42二进制拆分:

\[(42)_{10}=(101010)_2=1\times 2^5+0\times 2^4+\cdots+0\times 2^0 \]

那么,\(3^{42}\)就变成了:

\[3^{42}=3^{1×32 + 0×16 + 1×8 + 0×4 + 1×2 + 0×1} \]

所以,我们就有了这样的一个原理:

在求解\(a^b\)的时候,如果\(b\)是奇数,那么原式就是:\(a\times a^{b-1}\)

同理,如果\(b\)是偶数,那么原式就可以变成:\(a^{b\div2}\times a^{b\div 2}\).

这是一种倍增的思想,这样我们就把原来的\(O(b)\)算法就被优化成了\(O(logb)\)的算法。

这就是快速幂的实现原理。


快速幂的代码实现

根据我们刚刚学习的快速幂的实现原理,我们很容易发现,这个东西可以用递归来实现。代码如下:

int qpow(int a,int b)
{
    if(!b)
        return 1;
    else if(b&1)
        return a*qpow(a,b-1);
    else
    {
        int t=qpow(a,b>>1);
        return t*t;
    }
}

但是,学过递归的小伙伴应该知道,递归的常数巨大无比。所以上面的代码并不是一个资深\(OIer\)会选择的东西。

那快速幂怎么写保证常数不大呢?

我们采用一种迭代的写法:

我们会发现,无论\(b\)为何值,它在快速幂迭代的过程中要么\(-1\),要么\(\div 2\)。但无论它采用了以上的哪一种操作,都必会有一个时刻,\(b=1\)。,也就是说,\(b\)在迭代的过程中,至少会有一个时刻\(b\)为奇数。

那么我们考虑,我们完全可以在\(b\div 2\)的迭代中,先不使迭代的结果影响到答案,而是先把迭代的结果储存下来,然后等到\(b\)为奇数的时候统一加到答案里去。这样就省去了繁琐的递归和记录答案的过程,保证了常数小,而且维护了答案的正确性。

代码如下:

int qpow(int a,int b)
{
    int ret=1;
    while(b>0)
    {
        if(b&1)
            ret*=a;
        a*=a;
        b>>=1;
        }
    }
    return ret;
}

快速乘的原理及其代码实现

其实,就是把快速幂的乘法运算变成了加法运算。

原理超级容易理解...

模板也大同小异:

ll qmult(ll a,ll b)
{
    ll ret=0;
    while(b>0)
    {
        if(b&1)
            ret=(ret+a)%mod;
        a=(a+a)%mod;
        b>>=1;
    }
    return ret;
}
posted @ 2019-09-27 19:27  Seaway-Fu  阅读(1272)  评论(1编辑  收藏  举报