GDUT ACM2022寒假集训 专题四 C(求逆元)(exgcd & 费马小定理)

一、为什么要求逆元

先引入求余运算法则

( 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( (ab)%p=(a%pb%p)%p( ) ) )

( a ∗ b ) % p = ( a % p ∗ b % p ) % p ( \left(a^{ * } b\right) \% p=(a \% p * b \% p) \% p \quad( (ab)%p=(a%pb%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} bx%m=1 b ⋅ x = 1 (   m o d   m ) \bm{b \cdot x=1(\bmod m)} bx=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=ainv(b)%p=(a%pinv(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) ap11(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) ainv(a)1ap1(modp)

于是有 i n v ( a ) ≡ a p − 2 (   m o d   p ) i n v(a) \equiv a^{p-2}(\bmod p) inv(a)ap2(modp)

于是我们只需要对 a p − 2 a^{p-2} ap2求一下快速幂即可得到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} n106

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!(ni)!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!)%pinv((ni)!)%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;
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值