快速幂
正常情况下求一个数的幂时间复杂度为O(n),而快速幂能把时间复杂度降到O(logn)。
举个例子:求5的13次方
思想
首先把13化为二进制:1101,即13 = 1101 = 8 * 1 + 4 * 1 + 2 * 0 + 1 * 1 。
即
5
13
=
5
8
∗
1
+
5
4
∗
1
+
5
2
∗
0
+
5
1
∗
1
5^{13} = 5^8 * 1 + 5^4 *1 +5 ^ 2 * 0 + 5^1 * 1
513=58∗1+54∗1+52∗0+51∗1
接下来就是找规律时间:
先把红色的1101 扔到一边,单独看
5
1
,
5
2
,
5
4
,
5
8
5^1,5^2,5^4,5^8
51,52,54,58这四个数,这四个数有一个很明显的规律,后一个数等于前一个数的平方。
如果我知道第一个数
5
1
=
5
5^1 = 5
51=5,
那很轻松就能求出第二个数是5 * 5 = 25,
接着,第三个数 25 * 25 = 625,
第四个数 625 * 625 = 390625
如果后边还有更多,可以接着往下求。
上述过程用一个循环就能解决:
long long ans = 1
long long x = 5
long long n = 4
while(n){
ans = ans * x
x *=x;
n--;
}
循环结束之后,ans的值就是5 * 25 * 625 * 390625的值,也就是
5
1
∗
5
2
∗
5
4
∗
5
8
5^1 * 5^2 * 5^4 * 5^8
51∗52∗54∗58的值。
现在再看如果把1101 加上该怎么算。
可以发现1对结果没有任何影响:
5
1
∗
1
和
5
1
5^1 * 1 和 5^1
51∗1和51没有区别。
再看0产生的影响:
5
2
∗
0
=
5
0
=
1
5^2 * 0 = 5^0 = 1
52∗0=50=1,即如果是0,会把该项变成1。
那要对上面的代码稍作更改,就可以求带1101的式子的积:
long long ans = 1
long long x = 5
long long n = 4
long long arr = [1, 0, 1, 1]
int i = 0
while(n){
if(arr[i]){
ans = ans * x
}
i++;
x *=x;
n--;
}
这样计算出来的就是
5
8
∗
1
∗
5
4
∗
1
∗
5
2
∗
0
∗
5
1
∗
1
=
5
13
5^8 * 1 * 5^4 * 1 * 5^2 * 0 * 5^1 * 1 = 5^{13}
58∗1∗54∗1∗52∗0∗51∗1=513的值。
以上就是快速幂的思想:把指数化成二进制,然后根据这个二进数每一位上是0还是1来判断进行什么操作。
但是上面的代码显然是没法使用的,因为我们所有的变量都是写死的,那接下来就来看看怎么把思想变成代码。
代码实现
整个快速幂一共要做的就是两件事:
1.把指数化成二进制形式。
2.根据这个二进数每一位上是0还是1来判断进行什么操作。
一个很容易想到的方法是把两步分开,先化二进制,然后把二进制存成数组或者字符串形式,再来判断。
#include<cstdio>
#include<cstring>
#include<cmath>
#include<iostream>
#define ll long long
const int MOD=1e7+7;
using namespace std;
int arr[50];//保存指数转化后的二进制
int cnt = 1;
ll ans = 1;//存储最终结果
void Get_Binary(ll n)//转化成二进制
{
while(n > 0){
arr[cnt++] = n % 2;
n /= 2;
}
}
ll Get_ans(ll x){//求结果
cnt--;
while(cnt){
if(arr[cnt]){
ans = ans * x;
}
x *= x;
cnt--;
}
return ans;
}
int main()
{
ll x,n;
cin >> x >> n;
Get_Binary(n);
for(int i = 1; i < cnt; i ++){
cout << arr[i];
}
cout << endl;
cout << Get_ans(x) << endl;
}
//以5 13输入为例
//上边的代码第35行输出的是 1 0 1 1(可以发现是反着存储的(13化二进制是1101),所以求结果的时候也要反着来)
//然后38行输出 1220703125
上边的方法自然是可行的,时间复杂度也是两次log2n的循环。
不过还是可以小小的优化一下。
其实完全没必要把二进制结果存下来,因为这个东西只用到了一次,而且用二进制的每一位的时候和它前后都无关,所以我们完全可以在求二进制每一位的时候同时进行结果的运算。
#include<cstdio>
#include<cstring>
#include<cmath>
#include<iostream>
#define ll long long
const int MOD=1e7+7;
using namespace std;
ll ans = 1;//存储最终结果
ll Get_Binary(ll x, ll n)//转化成二进制
{
while(n > 0){
if(n % 2){
ans = ans * x;
}
x *= x;
n /= 2;
}
return ans;
}
int main()
{
ll x,n;
cin >> x >> n;
cout << Get_Binary(x, n) << endl;
}
上面就几乎是标准的快速幂写法了。接下来还有两个小地方补充:
- 第十二行的n % 2 可以换成 n & 1 ,两者是可以划等号的。同样,第十六行的n /= 2也可以换成 n >>= 1,这个操作是把二进制右移一位,也就相当于除以2。之说以这样写据说是计算机中进行二进制运算比十进制要快那么一丢丢。
- 求快速幂一般都会伴随着取余,否则根本存不下。上面的代码如果改的话只需要把第13和15行改一下就可以了:
while(n > 0){
if(n % 2){
ans = (ans * x) % MOD;
}
x = (x * x) % MOD;
n /= 2;
}
之所以可以这样写是因为取模的法则:
- (a + b) % p = (a % p + b % p) % p
- (a - b) % p = (a % p - b % p ) % p
- (a * b) % p = (a % p * b % p) % p