快速幂第一章(C++递归版本)
当题目要我们求类如 a b = ? a^b=? ab=?的题目时,最直接,最暴力的写法如下:
#include <bits/stdc++.h>
using namespace std;
#define LL long long
int main() {
LL a,b,res=1;
cin>>a>>b;
while(b--) {
res*=a;
}
cout<<res;
}
算法简单明了,循环 b b b次,每次答案 × a ×a ×a。而这类代码,往往是TLE。
TLE很好理解。这段代码的时间复杂度是 O ( b ) O(b) O(b)。当b的数据范围很大时自然会超时。
时间复杂度如何计算可以参考编者的另一篇博客大 O O O时间复杂度详解
关于幂的知识
- a b = a × a × . . . × a × a a^b=a×a×...×a×a ab=a×a×...×a×a( b b b个 a a a相乘)
-
a
x
×
a
y
a^x×a^y
ax×ay
= a × a × . . . × a \red a\red ×\red a\red×\red.\red.\red.\red×\red a a×a×...×a ( x (x (x个 a ) a) a) × × × a × a × . . . × a × a \blue a\blue ×\blue a\blue ×\blue .\blue .\blue .\blue ×\blue a\blue ×\blue a a×a×...×a×a ( y (y (y个 a ) a) a)
= ( x + y ) (x+y) (x+y)个 a a a相乘的积
= a a ax+y
例如 2 3 ∗ 2 5 = 2 8 2^3*2^5=2^8 23∗25=28
正文
有了以上的性质,我们来分析这个式子:
当 b b b为偶数时,
a b = a a^b=a ab=ab/2 ∗ * ∗ a a ab/2
很好理解,因为 b / 2 + b / 2 = b b/2+b/2=b b/2+b/2=b。
当 b b b为奇数时呢?肯定不能像偶数那样操作,直接 ÷ 2 ÷2 ÷2
我们思考小学数学的一个概念,所有奇数都可以表示为 2 n + 1 2n+1 2n+1的形式
假设这个奇数为 x x x,
x
=
2
n
+
1
x=2n+1
x=2n+1
2
n
=
x
−
1
2n=x-1
2n=x−1
n
=
(
x
−
1
)
/
2
n=(x-1)/2
n=(x−1)/2
n
=
x
/
2
−
0.5
n=x/2-0.5
n=x/2−0.5
我们想一下, x x x是一个奇数,所以 x / 2 x/2 x/2的小数部分必然是 . 5 .5 .5, x / 2 − 0.5 x/2-0.5 x/2−0.5相当于去掉了 x / 2 x/2 x/2的小数部分,也就是将 x / 2 x/2 x/2向下取整。
所以,奇数 x = ⌊ x / 2 ⌋ + ⌊ x / 2 ⌋ + 1 x=⌊x/2⌋+⌊x/2⌋+1 x=⌊x/2⌋+⌊x/2⌋+1。
⌊ x / 2 ⌋ ⌊x/2⌋ ⌊x/2⌋表示 x / 2 x/2 x/2的商向下取整。
所以, 对于任意奇数 b b b, a b = a a^b=a ab=a⌊b/2⌋ ∗ * ∗ a a a⌊b/2⌋ ∗ * ∗ a 1 a^1 a1 成立。
例:
2
6
=
2
3
∗
2
3
2^6=2^3*2^3
26=23∗23;
2
9
=
2
4
∗
2
4
∗
2
2^9=2^4*2^4*2
29=24∗24∗2
这时,我们会惊奇的发现,
求 a b a^b ab的问题就可以根据 b b b的奇偶性转化:
- 若
b
b
b是奇数,将
a
b
a^b
ab的问题转化成求
a a a⌊b/2⌋ ∗ * ∗ a a a⌊b/2⌋ ∗ * ∗ a a a
2.若 b b b是偶数, 将 a b a^b ab的问题转化成求 a a ab/2 ∗ * ∗ a a ab/2
因为 a a ab/2的求解规模一定比求 a b a^b ab小,所以转化是有意义的,减少了运算规模。
有了思路,我们需要考虑一下,用什么算法呢?
我们回到前面,我们是将大任务转化成小任务求解。 a b a^b ab的目标问题可以根据子问题( a a ab/2)求解,对于这类问题,我们通常使用递归算法求解。
如果不会递归算法,可以自己上网查阅相关资料 。
初步代码
#include <bits/stdc++.h>
using namespace std;
#define LL long long
LL F(LL a,LL b) {
if(b==1) return a; //递归边界 a^1=a
if(b%2==1) return F(a,b/2)*F(a,b/2)*a; //指数是奇数
else return F(a,b/2)*F(a,b/2); //指数是偶数
}
int main() {
LL a,b;
cin>>a>>b;
cout<<F(a,b);
}
程序的优化
上面的代码思路是完全正确的,但是程序真正运行起来感觉速度并没有快很多。
我们画一个图来理解我们的程序。
假设我们要求
2
8
2^8
28,我们就要先求
2
4
2^4
24,
想要求
2
4
2^4
24 , 我们就要求
2
2
2^2
22,
想要求
2
2
2^2
22 , 我们就要求
2
1
2^1
21。
画成图就是这样的:
1
1
1
2
8
2^8
28
2
2
2 / \
3
3
3
2
4
2^4
24 ×
2
4
2^4
24
4
4
4 / \ / \
5
5
5
2
2
2^2
22 ×
2
2
2^2
22
2
2
2^2
22 ×
2
2
2^2
22
6
6
6 / \ / \ / \ / \
7
7
7
2
1
2^1
21 ×
2
1
2^1
21
2
1
2^1
21 ×
2
1
2^1
21
2
1
2^1
21 ×
2
1
2^1
21
2
1
2^1
21 ×
2
1
2^1
21
我们可以发现第 3 3 3行 2 4 2^4 24算了两次,而算 2 4 2^4 24的代价是非常大的,我们完全没有必要算两遍 2 4 2^4 24,算一遍,将它的结果记录下来备用就可以了。
修改后的代码:
#include <bits/stdc++.h>
using namespace std;
#define LL long long
LL F(LL a,LL b) {
if(b==1) return a; //递归边界 a^1=a
int res=F(a,b/2); //记录答案
if(b%2==1) return res*res*a; //指数是奇数
else return res*res; //指数是偶数
}
int main() {
LL a,b;
cin>>a>>b;
cout<<F(a,b);
}
这个程序算 2 8 2^8 28画成图是这样的:
1
1
1
2
8
2^8
28
2
2
2 /
3
3
3
(
2
4
)
2
(2^4)^2
(24)2
4
4
4 /
5
5
5
(
2
2
)
2
(2^2)^2
(22)2
6
6
6 /
7
7
7
(
2
1
)
2
(2^1)^2
(21)2
这个算法的时间复杂度是 O ( l o g \red O\red (\red l\red o \red g O(log b ) \red b\red ) b)
您听懂了吗?
写作不易,不喜勿喷,留个赞吧!