分治算法——快速幂(平方求幂)
什么是快速幂呢,我们先来看维基百科对快速幂的解释:
在数学和程序设计中,平方求幂(英语:exponentiating by squaring)或快速幂是快速计算一个数(或更一般地说,一个半群的元素,如多项式或方阵)的大正整数幂的一般方法。这些算法可以非常通用,例如用在模算数或矩阵幂。对于通常使用加性表示法的半群,如密码学中使用的椭圆曲线,这种方法也称为double-and-add。
当我们遇到一个问题需要让我们求得一个数 n 的 m 次方,那么最简单的方法是将 n 乘以 m 次,得到结果,这样的方式对于计算诸如 2^10 这样的数学公式比较简单但是如果我们现在需要计算的是 2^10000000 这样的式子呢,在我的电脑上,这样的式子需要运行 0.3s 左右,显然如果我们的程序需要计算 2 的更高次方的时候这样的算法对于算法竞赛而言时间上显然是不允许的,其实在计算这样的式子的时候有大量的运算步骤是可以避免的,我们现在拿 2^16 次方举例:
按照常规算法,我们的计算式如下:
2
16
=
2
2
∗
2
2
∗
2
2
∗
2
2
∗
2
2
∗
2
2
∗
2
2
∗
2
2
∗
2
2
∗
2
2
∗
2
2
∗
2
2
∗
2
2
∗
2
2
∗
2
2
∗
2
2
2^{16} = 2^2 * 2^2 * 2^2 * 2^2 * 2^2 * 2^2 * 2^2 * 2^2 * 2^2 * 2^2 * 2^2 * 2^2 * 2^2 * 2^2 * 2^2 * 2^2
216=22∗22∗22∗22∗22∗22∗22∗22∗22∗22∗22∗22∗22∗22∗22∗22
按照上面的计算式,计算这样的式子,我们需要进行 16 次计算。
那么如果我们换一种思路来计算呢:
2
16
=
2
8
∗
2
8
2^{16} = 2^8 * 2^8
216=28∗28
2 8 = 2 4 ∗ 2 4 2^8 = 2^4 * 2^4 28=24∗24
2 4 = 2 2 ∗ 2 2 2^4 = 2^2 * 2^2 24=22∗22
2 2 = 2 ∗ 2 2^2 = 2 * 2 22=2∗2
通过上面这样的计算式我们计算这样的式子我们计算步骤只需要 4 步,我们在计算的过程中省略了大量的无用的步骤,这样就可以提高程序的时间效率,能让程序在计算幂的过程中的时间更加短,同样我们再来看一下 2^13 的计算:
先来采用最常规的做法来计算:
2
13
=
2
2
∗
2
2
∗
2
2
∗
2
2
∗
2
2
∗
2
2
∗
2
2
∗
2
2
∗
2
2
∗
2
2
∗
2
2
∗
2
2
∗
2
2
2^{13} = 2^2 * 2^2 * 2^2 * 2^2 * 2^2 * 2^2 * 2^2 * 2^2 * 2^2 * 2^2 * 2^2 * 2^2 * 2^2
213=22∗22∗22∗22∗22∗22∗22∗22∗22∗22∗22∗22∗22
现在我们用快速幂的算法来计算这样的式子,我们采取计算步骤最少的方法来计算:
2
13
=
2
8
∗
2
4
∗
2
1
2^{13} = 2^8 * 2^4 * 2^1
213=28∗24∗21
2 8 = 2 4 ∗ 2 4 2^8 = 2^4 * 2^4 28=24∗24
2 4 = 2 2 ∗ 2 2 2^4 = 2^2 * 2^2 24=22∗22
2 2 = 2 ∗ 2 2^2 = 2 * 2 22=2∗2
通过上面的计算式,我们得到结果只需要进行 5 步就能得到结果,那么 2^13 的 13 是如何的得到的呢,我们可以看一下如下的式子:
13
=
2
3
+
2
2
+
2
1
13 = 2^3 + 2^2 + 2^1
13=23+22+21
通过这样的计算式,我们是要比正常的连乘计算要更加简单快速,并且可以将正常的运算时间复杂度从优化 O(n) 到 O(logn)。
这是最基本的式子,我们现在将这个式子推广到 a^b 的情况:
我们可以令:
b
=
p
0
2
k
+
p
1
2
k
−
1
+
.
.
.
+
p
k
2
0
b = p_02^k + p_12^{k-1} + ... + p_k2^0
b=p02k+p12k−1+...+pk20
那么原来的式子就可以转换为:
我们先采用递归的方式实现快速幂,代码如下:
#include<map>
#include<list>
#include<cmath>
#include<queue>
#include<stack>
#include<vector>
#include<cstdio>
#include<iomanip>
#include<cstring>
#include<iterator>
#include<iostream>
#include<algorithm>
#define R register
#define LL long long
#define pi 3.141
using namespace std;
inline int quick_pow(int a, int b){
if(b == 1){
return a;
}
else{
int c = quick_pow(a, b / 2);
if(b % 2 == 0){
return c * c;
}
else{
return c * c * a;
}
}
}
int main(){
int a, b;
scanf("%d%d", &a, &b);
printf("%d", quick_pow(a, b));
return 0;
}
这样我们就完成了递归形式的快速幂,我们现在模拟一下递归形式的快速幂的运行方式。
如果要计算 2^13
b > 1 所以进入函数 quick_pow(2, 6) ;此时 b > 1 仍然成立,进入函数 quick_pow(2, 3);此时 b > 1 仍然成立,进入函数 quick_pow(2, 1);此时 b = 1 函数返回 2 ;当 b = 3 时 b 是奇数,返回 8 ;当 b = 6 时 b 是偶数,返回 64 ;当 b = 13 时 b 是奇数,返回 64 * 64 * 2,得到结果 8192。通过如上的步骤,得到结果 8192。
我们可以通过下面的图片来理解快速幂的求法:
既然在编写快速幂的时候运用了递归和大量除以 2、取余的操作,这时我们不嫩难想到用位运算优化快速幂,其中 >> 运算符可以将二进制去掉最后一位,也就是将原来的数字除以 2 ,同时 & 运算符可以取得二进制的末尾,可以判断原来数字的奇偶例如:number & 1 == 1 为奇数,number & 1 == 0为偶数。通过运用位运算和非递归优化快速幂,可以使快速幂的效率进一步提高。
示例代码如下:
#include<map>
#include<list>
#include<cmath>
#include<queue>
#include<stack>
#include<vector>
#include<cstdio>
#include<iomanip>
#include<cstring>
#include<iterator>
#include<iostream>
#include<algorithm>
#define R register
#define LL long long
#define pi 3.141
using namespace std;
inline int quick_pow(int a, int b){
int answer = 1, base = a;
while(b){
if(b & 1){
answer *= base;
}
base *= base, b >>= 1;
}
return answer;
}
int main(){
int a, b;
scanf("%d%d", &a, &b);
printf("%d", quick_pow(a, b));
return 0;
}
我们同样通过 2^13 来模拟这个过程:
通过上面的优化可以很完美的实现快速幂,效率得到很大的提升,当然快速幂还可以继续进行优化,优化成矩阵快速幂经过矩阵快速幂的优化,快速幂的时间复杂度可以降到线性时间复杂度大致为 O(NMP) ,在之后会另行讲解。