摘要
本文讲解了快速幂算法的定义、复杂度证明及两种实现(递归与非递归),以及它的两个重要拓展:快速幂模M算法和矩阵快速幂。其中矩阵快速幂算法是矩阵求幂问题对整数求幂问题的借鉴,实际应用中对于线性递推式求解能起到强大的效率优化。
快速幂算法
问题引入:求
an(a,n∈N+)
朴素算法:令ans初始值为1,乘n次a得到
an
朴素算法时间复杂度:O(n)
问题:如果n非常大,比如高达
1015
,怎么办?
思考:朴素算法哪里可以优化?
朴素算法的特点是,连乘过程中底数始终为
a
,这很不聪明。考虑下例:
我们没有必要乘15次2,注意到
15=23+22+21+20
,不妨将
215
拆分为
223×222×221×220
然后看一下n的二进制形式:
15=(1111)2
.为什么要看这个呢?再举一个例子。
a=2,n=6
26=222×221
,而
6=(110)2
发现什么规律?
观察发现,当n的第i位(从低到高,i>=0)为1时,
an
的拆解表达式里就要乘上一项
a2i
.
于是我们有了朴素算法改进的思路:逐位判断幂次n的二进制位是否为1,若是,给答案乘上一个
a2i
.
改进后的算法的伪代码描述如下:
a, n, ans;
ans=1;
while n>0
a = a*a;
if n%2
ans = ans*a;
n = n/2;
这个算法的时间复杂度是多少呢?很显然,它取决于n的二进制形式有多少位,因此 T(n)=⌈log2(n)⌉=O(logn) .这就是快速幂算法。
其实快速幂算法还可以递归实现,因为:
当n为偶数时,
an=(an/2)2
当n为奇数时,
an=(an/2)2×a
边界条件:当n=1,答案为
a
C语言描述如下:
int quick_power(int a, int n)
{
if(n == 1) return a;
int x = quick_power(a, n/2);
long long ans = (long long)x*x;
if(n%2) ans *= a;
return (int)ans;
}
代码中加入了防溢出处理,用快速幂算法的时候比较容易犯的一个错误就是忘了考虑溢出,因此在使用的时候要看清楚数据范围,估算一下答案上界。另外,快速幂算法不推荐用递归实现,因为非递归版本不但代码也很简洁,而且效率还更优。
拓展一:快速幂模M算法
有时候所求幂的结果可能很大,于是问题要求对结果模上一个数M。我们只需要在原来算法的基础上运用一下模运算的性质即可。所谓模运算性质是指以下两条:
∏ni=1ai mod M=(∏ni=1ai mod M)mod M
算法非递归实现的伪代码描述为
a, n, ans, M;
ans=1;
while n>0
a = a*a % M;
if n%2
ans = ans*a % M;
n = n/2;
拓展二:矩阵快速幂算法
快速幂算法解决的是整数求幂的问题,而矩阵快速幂解决的是矩阵求幂问题,两者没有本质的区别。如果用C++实现,我们只要定义一个矩阵类,然后重载一下乘法运算符,原先的快速幂算法几乎不需要改变。
矩阵快速幂常常用于线性递推式的加速。以下仅举一例。
快速求斐波那契数列第n项
对于这个问题,普通求法的复杂度是O(n),现在我们用矩阵快速幂将它优化到O(logn).
首先,将递推式
f(n)=f(n−1)+f(n−2)
改写成矩阵形式
进而得到
接下来,用矩阵加速算法求出 [1110]n−4 ,再做一次矩阵乘法,所得矩阵的(0,0)元素就是最终结果。
对于更一般的线性递推式,构造其加速矩阵的方式超出了本文的论述范围,在此省略。