一、为什么要求逆元
先引入求余运算法则
( a + b ) % p = ( a % p + b % p ) % p ( (a+b) \% p=(a \% p+b \% p) \% p \quad( (a+b)%p=(a%p+b%p)%p( 对 ) ) )
( a − b ) % p = ( a % p − b % p ) % p ( (a-b) \% p=(a \% p-b \% p) \% p \quad( (a−b)%p=(a%p−b%p)%p( 对 ) ) )
( a ∗ b ) % p = ( a % p ∗ b % p ) % p ( \left(a^{ * } b\right) \% p=(a \% p * b \% p) \% p \quad( (a∗b)%p=(a%p∗b%p)%p( 对 ) ) )
( a / b ) % p = ( a % p / b % p ) % p ( (a / b) \% p = (a\%p / b\%p) \%p \quad( (a/b)%p=(a%p/b%p)%p( 错 ) ) )
那么为什么只有除法这么做是错的呢,上面三种正确的证明我暂且不提
关于错误的除法求余运算,我们只需要举一个反例,如
( 100 / 50 ) % 20 = 2 ≠ [ ( 100 % 20 ) / ( 50 % 20 ) ] % 20 = 0 \bm{(100 / 50) \% 20=2 \neq[(100 \% 20) /(50 \% 20)] \% 20=0} (100/50)%20=2=[(100%20)/(50%20)]%20=0
对于一些结果或者过程数字较大的题目,为了防止计算机无法存储,我们不能只在最后取模,往往需要在计算过程中一并取模,如果过程中出现了除法,这个时候便需要逆元
对于 a n s = ( a / b ) % m a n s=(a / b) \% m ans=(a/b)%m
为了解决这种模意义下的除法问题,我们就需要对b求他的逆元,将 a/b 变为 a·x
此时 b ⋅ x % m = 1 \bm{b \cdot x \% m=1} b⋅x%m=1 即 b ⋅ x = 1 ( m o d m ) \bm{b \cdot x=1(\bmod m)} b⋅x=1(modm),这里的x的效果就和b的倒数是一样的
我们称x为b关于m的逆元,或者说x和b关于m互为逆元,b的逆元我们用 i n v ( b ) inv(b) inv(b)来表示
那么开头的关于除法的取模问题,我们可以转变成
a / b % p = a ⋅ inv ( b ) % p = ( a % p ⋅ inv ( b ) % p ) % p a / b \% p=a \cdot \operatorname{inv}(b) \% p=(a \% p \cdot \operatorname{inv}(b) \% p) \% p a/b%p=a⋅inv(b)%p=(a%p⋅inv(b)%p)%p
这样我们就可以把除法问题完全转化为乘法问题了
注意:b和m互质,b才有关于m的逆元
二、三种求逆元的方法
下面介绍两种求逆元的方法:扩展欧几里得,费马小定理
1、扩展欧几里得算法
直接把模板原封不动搬过来
long long x, y; //目前方程真正的解
void exgcd(long long a, long long b)
{
//当前目的:求解 ax + by = gcd(a, b) 这么一个方程
if (b == 0) // a, b不断改变的过程中,b最终必然会成为0
{
//在 b = 0 时方程还要成立? 使 x = 1, y = 0 ,必然成立
x = 1;
y = 0;
return;
}
exgcd(b, a % b); //把下一层系数传进去(先求下一个方程的解 )
//现在我们已经拿到了下一个方程的解x, y
long long tx = x; //暂时存一下x,别丢了,tx即xn
x = y; //此时的x为xn-1 y为yn
y = tx - a / b * y;//等号左侧为yn-1 右侧为yn
}//xn-1=yn yn-1=xn-[a/b]*yn
int main()
{
int a;
exgcd(a,mod);
cout<<x;
}
对于以上模板来说exgcd(a,mod);表示求a关于mod的逆元,经过扩展欧几里得算法的计算后
得到的x表示a关于mod的逆元,得到的y表示mod关于a的逆元
2、费马小定理
若 p p p是质数,且 g c d ( a , p ) = 1 gcd(a,p)=1 gcd(a,p)=1,则有 a p − 1 ≡ 1 ( m o d p ) a^{p-1} \equiv 1 (\bmod p) ap−1≡1(modp)
从逆元的定义推导,则有 a ⋅ inv ( a ) ≡ 1 ≡ a p − 1 ( m o d p ) a \cdot \operatorname{inv}(a) \equiv 1 \equiv a^{p-1}(\bmod p) a⋅inv(a)≡1≡ap−1(modp)
于是有 i n v ( a ) ≡ a p − 2 ( m o d p ) i n v(a) \equiv a^{p-2}(\bmod p) inv(a)≡ap−2(modp)
于是我们只需要对 a p − 2 a^{p-2} ap−2求一下快速幂即可得到a关于p的逆元
本方法只对p为质数的情况有效
long long qpow(long long a, long long n, long long p)// 快速幂
{
long long ans = 1;
while (n)
{
if (n & 1)
ans = ans % p * a % p;
a = a % p * a % p;
n >>= 1;
}
return ans;
}
long long inv(long long a, long long p)
{
return qpow(a, p - 2, p);//求a的p-2次方模p的值
}
int main()
{
int a, mod;
inv(a, mod);
}
例题一(同余方程)
原题链接:https://vjudge.net/contest/479577#problem/C
1、题干
Vitaly有一些奇怪的癖好,比如他特别爱两个小于10的数字a和b。Vitaly定义十进制表示下每一位都是a或b的数为“好数”,一个每一位数加起来为“好数”的“好数”被称为“极好的数”。
举个栗子=w=,如果偏爱数字为1和3,那么1212不是“好数”,13和311是“好数”,111是“极好的数”。
现在Vitaly想知道,长度为n(长度不包括前导0)的“极好的数”有多少个。答案对1e9+7取模。
n ≤ 1 0 6 n≤10 ^{6} n≤106
2、输入格式
一行一个整数n,表示长度为n
3、输出格式
一行一个整数,意义如题
4、样例
sample input 1
1 3 3
sample output 1
1
sample input 2
2 3 10
sample output 2
165
例题一题解
1、分析
对于本题我们不能直接枚举个数,观察题目可知,一个数是否是极好的数与他每一位数的前后顺序无关
我们只需要枚举出可以组成极好的数的a和b的每一种组合,再乘上他们的组合数即可得到当前a和b的组合一共可以组成多少极好的数
对于一个长度为n的数,我们可以假设他由 i 个a和 n-i 个b组成,我们可以将a的位置固定不变,然后将b使用插空法放入a之间,求组合数
C
n
i
=
n
!
i
!
(
n
−
i
)
!
C_{n}^{i}=\frac{n !}{i !(n-i) !}
Cni=i!(n−i)!n!
对于分母部分我们需要使用逆元将其转化为乘法运算
( n ! % p ⋅ ( inv ( i ! ) % p ⋅ i n v ( ( n − i ) ! ) % p ) % p ) % p (n ! \% p \cdot(\operatorname{inv}(i !) \% p \cdot i n v((n-i) ! ) \% p) \% p) \% p (n!%p⋅(inv(i!)%p⋅inv((n−i)!)%p)%p)%p
最后输出ans时,虽然我们在计算过程中已经进行取模操作,但是我们仍然无法保证ans在加和过程中不大于题目模数,所以在最后输出时还需要再进行一次取模
2、代码
#include <bits/stdc++.h>
using namespace std;
const long long mod = 1e9 + 7;
long long f[1000001];
long long x, y, ans; //目前方程真正的解
int a, b, n;
void exgcd(long long a, long long b)//扩展欧几里得求逆元
{
if (b == 0)
{
x = 1;
y = 0;
return;
}
exgcd(b, a % b);
long long tx = x;
x = y;
y = tx - a / b * y;
}
bool judge(int x) //判断由i个a和(n-i)个b组成的数字是否每一位相加后得到的数字都是a或者b组成
{
int k;
while (x > 0)
{
k = x % 10;
if (k != a && k != b)
return 0;
x /= 10;
}
return 1;
}
int main() //(a / b) % p = (a * inv(b) ) % p = (a % p * inv(b) % p) % p
{
cin >> a >> b >> n;
f[0] = 1;
for (int i = 1; i <= n; i++)
f[i] = f[i - 1] * i % mod; //将每个数i的阶乘求出并取模,方便后序使用
for (int i = 0; i <= n; ++i) //枚举i和n的各种组合,i个a和(n-i)个b
{
if (judge(a * i + b * (n - i))) //判断该种组合是否符合条件
{
long long x1 = 0, x2 = 0;
x = 0, y = 0;
exgcd(f[i], mod); //扩展欧几里得算法求逆元
x1 = (x + mod) % mod;
x = 0, y = 0;
exgcd(f[n - i], mod); //扩展欧几里得算法求逆元
x2 = (x + mod) % mod;
ans += (f[n] * (x1 % mod * x2 % mod)) % mod;
}
}
cout << ans % mod; //输出时再取模一次
return 0;
}