第二十七章 快速幂与扩展欧几里德算法
- 一、快速幂
- 1、使用场景
- 2、算法思路
- (1)二进制优化思想
- (2)模运算法则
- 3、代码实现
- (1)问题
- (2)代码
- 二、快速幂求逆元
- 1、什么是逆元?
- (1)同余
- (2)逆元
- 2、逆元的求法
- (1)欧拉定理
- (2)费马小定理
- (3)问题
- (4)求解逆元
一、快速幂
1、使用场景
我们知道,如果我们想计算一个 q k q^k qk,我们可以不断地去乘,但这样的时间复杂度是 O ( k ) O(k) O(k),这个是复杂度就很高了。
而我们的快速幂算法就是为了优化这个过程的,快速幂算法的时间复杂度是 O ( l o g k ) O(logk) O(logk)。
2、算法思路
(1)二进制优化思想
快速幂的优化思想其实就是二进制的优化思想。
我们在动态规划中,解决多重背包问题的时候,我们就用到了二进制优化的思想。
那么什么是二进制优化呢?
比如一个数
120
120
120
这个数的二进制表示是:
01111000
0111 1000
01111000
为了更快的表示这个数,我们可以提前准备好这样几个数:
1000 0000,
0100 0000,
0010 0000,
0001 0000,
0000 1000,
0000 0100,
0000 0010,
0000 0001
上面这些数字有什么特点呢?
就是我们任意组合几个上述所写的数字,都能让对应的位置变成1。
那么我们要是想得到我们的120,则需要让对应的位置变成1。
那么我们就需要让0000 0000加上
0100 0000
0010 0000
0001 0000
0000 1000
这几个数加在一起,就是0111 1000
也就是说我们从0算到120,只算了4次。
因此,我们预处理出这些数字:
q 1 , q 2 , q 4 . . . . . q^1,q^2,q^4..... q1,q2,q4.....
因为, q n ∗ q m = q^n*q^m= qn∗qm= q q q(n+m)
所以相当于我们在指数部分进行了二进制优化。
然后我们去遍历一个数的二进制表示,如果对应的二进制位是1,那么我们就从预处理的表中取出对应的数字。
(2)模运算法则
这种数学问题经常遇到取模操作,所以我们有必要了解一下常见的取模运算法则:
上述法则来自于百度百科的介绍。
3、代码实现
(1)问题
(2)代码
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL;
LL qmi(int a, int b, int p)
{
LL res = 1 % p;
while (b)
{
if (b & 1) res = res * a % p;
a = a * (LL)a % p;
b >>= 1;
}
return res;
}
int main()
{
int n;
scanf("%d", &n);
while (n -- )
{
int a, b, p;
scanf("%d%d%d", &a, &b, &p);
printf("%lld\n", qmi(a, b, p));
}
return 0;
}
二、快速幂求逆元
1、什么是逆元?
(1)同余
介绍逆元之前,先介绍一个新的概念:同余。
如果 a a a% c = m c=m c=m,并且 b b b% c = m c=m c=m,两个式子的结果是相同的,即余数相同,简称同余。
记作: a ≡ b ( m o d c ) a\equiv b(mod\ c) a≡b(mod c)
(2)逆元
在数论当中,除法是比较难处理的,因为除法意味着有可能会出现小数。
所以,我们想尽可能地避免除法的出现,而这也是逆元的作用。
如果 a ∣ b a|b a∣b并且 a b ≡ a ∗ x ( m o d c ) \frac{a}{b} \equiv a*x(mod\ c) ba≡a∗x(mod c),此时我们的 x x x就称作逆元,记作:b-1
由于二者是同余的,所以我们可以把求 a b m o d c \frac{a}{b}\ mod\ c ba mod c的过程,等价为, a ∗ x m o d c a*x\ mod\ c a∗x mod c
这样的话,我们就利用逆元避免了除法的出现。
那么现在的问题就是,我们如何求逆元呢?
2、逆元的求法
(1)欧拉定理
我们曾经介绍过欧拉函数,欧拉函数是用来求解互质的,而欧拉函数通常用 Φ ( n ) \Phi(n) Φ(n)来表示。
如果大家想要了解欧拉函数的话,可以去看作者之前的文章:
欧拉函数的介绍及证明
我们的欧拉定理也用到了欧拉函数。
欧拉定理的内容是:
如果 g c d ( a , n ) ,那么 a Φ ( n ) ≡ 1 ( m o d n ) 如果gcd(a,n),那么a^{\Phi(n)}\equiv 1(mod\ n) 如果gcd(a,n),那么aΦ(n)≡1(mod n)
(2)费马小定理
由欧拉函数可知,当 p p p是质数的时候, Φ ( p ) = p − 1 \Phi(p)=p-1 Φ(p)=p−1
所以,我们刚刚介绍的欧拉定理中:
如果 g c d ( a , n ) ,那么 a Φ ( n ) ≡ 1 ( m o d n ) 如果gcd(a,n),那么a^{\Phi(n)}\equiv 1(mod\ n) 如果gcd(a,n),那么aΦ(n)≡1(mod n)
如果 n n n为质数 p p p,并且 a a a和 p p p互质,那么 a p − 1 ≡ 1 ( m o d p ) a^{p-1}\equiv 1(mod \ p) ap−1≡1(mod p)
这个就是费马小定理,即 n n n为质数的时候的欧拉定理,可以说,费马小定理是欧拉定理的一个特殊情况。
(3)问题
(4)求解逆元
我们在回顾一下逆元的定义:
a
b
≡
a
∗
x
(
m
o
d
c
)
\frac{a}{b} \equiv a*x(mod\ c)
ba≡a∗x(mod c)
由于 b ≡ b ( m o d c ) b \equiv b(mod\ c) b≡b(mod c)
所以根据同余的一个计算性质:
a
b
∗
b
≡
a
∗
x
∗
b
(
m
o
d
c
)
\frac{a}{b}*b\equiv a*x*b(mod\ c)
ba∗b≡a∗x∗b(mod c)
(这个可以百度,作者这里就不证明了。)
所以上述的式子可以化简为:
a ≡ a ∗ x ∗ b ( m o d c ) a\equiv a*x*b(mod\ c) a≡a∗x∗b(mod c)
再次根据同余的运算性质,我们可以消掉两测的 a a a。
即, 1 ≡ x ∗ b ( m o d c ) 1\equiv x*b(mod\ c) 1≡x∗b(mod c)
整理一下即:
x ∗ b ≡ 1 ( m o d c ) x*b\equiv 1(mod\ c) x∗b≡1(mod c)
本道题中,特殊强调了模数是质数,也就是说我们的 c c c是质数,所以可以使用费马小定理。
由费马小定理:
b c − 1 ≡ 1 ( m o d c ) b^{c-1}\equiv 1(mod\ c) bc−1≡1(mod c)
整理一下:
b c − 2 ∗ b ≡ 1 ( m o d c ) b^{c-2}*b\equiv 1(mod\ c) bc−2∗b≡1(mod c)
所以我们的 x = b c − 2 x=b^{c-2} x=bc−2
所以现在这道题就转换成了快速幂。
因为题目中告诉我们 b b b和 c c c,我们需要计算的是 b c − 2 b^{c-2} bc−2。
用题目中给出的字母表示即:
a
p
−
2
a^{p-2}
ap−2
这是逆元存在的情况:什么情况下不存在呢?
我们化简后的表达式是:
x ∗ b ≡ 1 ( m o d c ) x*b\equiv 1(mod\ c) x∗b≡1(mod c)
如果说此时,b是c的倍数,那么b%c就是0,不是1。此时不成立。所以我们需要特殊判断一下。
题目说求1–p-1的答案,所以我们还是要取模一下。
代码:
#include<iostream>
using namespace std;
typedef long long ll;
long long qmi(ll a,ll b,ll p)
{
ll res=1;
while(b)
{
if(b&1)res=(res%p*a%p)%p;
a=(a%p*a%p)%p;
b>>=1;
}
return res;
}
int main()
{
int n;
cin>>n;
while(n--)
{
ll a,p;
cin>>a>>p;
if(a%p==0)puts("impossible");
else cout<<qmi(a,p-2,p)<<endl;
}
}