【数学】逆元

一、逆元的意义

在一道题中,如果既有除法又有取余,那么是不能直接除的:

\[\dfrac{a}{b}\bmod p\ne \dfrac{a\bmod p}{b\bmod p} \]

这时候逆元就登场了.

众所周知,\(a\cdot a^{-1}=1\),类似地定义 \(a\) 在模 \(p\) 意义下的逆元为使得

\[a\cdot x\equiv 1\pmod p \]

成立的 \(x\).

注意到当 \(\gcd(a,p)>1\) 时上式不可能成立,所以只有 \(a\)\(p\) 互质时才有逆元.

那么在模 \(p\) 意义下,\(x=a^{-1}\)\(x\) 记作 \(inv(a)\).

考虑回刚刚的问题:

\[\dfrac{a}{b}\bmod p=(a\cdot b^{-1})\bmod p \]

所以求出 \(inv(b)\),答案就是 \((a\cdot inv(b))\bmod p\).

二、求单个数的逆元

P2613 【模板】有理数取余

臭模数

用快读 \(+\) 取余读入.

首先 \(19260817\) 是个质数,所以当且仅当 \(19260817\mid b\)\(b\) 没有逆元,即无解.

以下讨论都在 \(\gcd(a,p)=1\) 的前提下.

1. 费马小定理

此情况仅适用于 \(p\) 为质数的情况.

由费马小定理知,当 \(p\) 为质数且 \(\gcd(a,p)=1\) 时有

\[a^{p-1}\equiv 1\pmod p \]

两边同除 \(a\)

\[a^{p-2}\equiv a^{-1}\pmod p \]

所以 \(inv(a)\) 其实就是 \(a^{p-2}\).

本题的答案就是 \(a\cdot b^{p-2}\).

\(\text{Code}\)

#include <iostream>
#include <cstdio>
#define ll long long
using namespace std;

const int MOD = 19260817;

int read()
{
	int x = 0;
	char c = getchar();
	while (c < '0' || c > '9')
	{
		c = getchar();
	}
	while (c >= '0' && c <= '9')
	{
		x = ((x << 3) + (x << 1) + c - '0') % MOD;
		c = getchar();
	}
	return x;
}

ll ksm(int a, int b)
{
	ll base = a, ans = 1;
	while (b)
	{
		if (b & 1)
		{
			ans = ans * base % MOD;
		}
		base = base * base % MOD;
		b >>= 1;
	}
	return ans;
}

ll inv(int a)
{
	return ksm(a, MOD - 2);
}

int main()
{
	int a = read(), b = read();
	if (!b)
	{
		puts("Angry!");
		return 0;
	}
	printf("%lld\n", a * inv(b) % MOD);
	return 0;
}

2. \(\rm exgcd\)

\(p\) 不为质数时也能用 \(\rm exgcd\) 求逆元.

要求出

\[a\cdot x\equiv 1\pmod p \]

相当于求出

\[ax+py=1 \]

的整数解.

注意:用 \(\rm exgcd\) 求出来的可能是负数,要转成正数.

\(\text{Code}\)

#include <iostream>
#include <cstdio>
#define ll long long
using namespace std;

const int MOD = 19260817;

int read()
{
	int x = 0;
	char c = getchar();
	while (c < '0' || c > '9')
	{
		c = getchar();
	}
	while (c >= '0' && c <= '9')
	{
		x = ((x << 3) + (x << 1) + c - '0') % MOD;
		c = getchar();
	}
	return x;
}

ll x, y;

void exgcd(int a, int b)
{
	if (!b)
	{
		x = 1, y = 0;
		return;
	}
	exgcd(b, a % b);
	ll tmp = x;
	x = y;
	y = tmp - a / b * y;
}

ll inv(int a)
{
	exgcd(a, MOD);
	x = (x % MOD + MOD) % MOD;
	return x;
}

int main()
{
	int a = read(), b = read();
	if (!b)
	{
		puts("Angry!");
		return 0;
	}
	printf("%lld\n", a * inv(b) % MOD);
	return 0;
}

以上两种算法的时间复杂度均为 \(\mathcal{O}(\log p)\).

当然根据欧拉定理有

\[a\equiv a^{\varphi(p)}\pmod p\\ a^{-1}\equiv a^{\varphi(p)-2}\pmod p \]

这样在 \(p\) 不为质数时也能求,但你还得用 \(\mathcal{O}(\sqrt{p})\) 求出 \(\varphi(p)\),没有实际意义,

三、线性求 \(1\sim n\) 的逆元

P3811 【模板】乘法逆元

单个求时间为 \(\mathcal{O}(n\log p)\),会炸.

1. 递推

首先 \(inv(1)=1\).

然后 \(p\) 可以表示成 \(iq+r\).

\[p\equiv0\pmod p\\ iq+r\equiv0\pmod p\\ r\equiv -iq\pmod p\\ r\cdot(i^{-1}r^{-1})\equiv-iq\cdot(i^{-1}r^{-1})\pmod p\\ i^{-1}\equiv-q\cdot r^{-1}\pmod p\\ i^{-1}\equiv-q\cdot(p\bmod i)^{-1}\pmod p\\ inv(i)\equiv-\left\lfloor\frac{p}{i}\right\rfloor\cdot inv(p\bmod i)\pmod p \]

然后就可以线性推了.

注意这样算出来会有负数,要转成正的.

\(\text{Code}\)

#include <iostream>
#include <cstdio>
#define int long long
using namespace std;

const int MAXN = 3e6 + 5;

int inv[MAXN];

signed main()
{
	int n, p;
	scanf("%lld%lld", &n, &p);
	inv[1] = 1;
	puts("1");
	for (int i = 2; i <= n; i++)
	{
		inv[i] = (-(p / i) * inv[p % i] % p + p) % p;
		printf("%lld\n", inv[i]);
	}
	return 0;
}

2. 阶乘逆元

此方法需要保证 \(n<p\).

首先,在 \(\bmod\ p\) 意义下

\[inv((i+1)!)=((i+1)!)^{-1}\\ ((i+1)!)^{-1}\cdot(i+1)=(i!)^{-1}=inv(i!)\\ \therefore inv((i+1)!)\cdot(i+1)=inv(i!) \]

所以先 \(\mathcal{O}(\log n)\) 算出 \(inv(n!)\),就可以 \(\mathcal{O}(n)\) 一直推到 \(inv(1!)\) 了.

然后有

\[\begin{aligned} inv(i)&=i^{-1}\\ &=(i!)^{-1}\cdot (i-1)!\\ &=inv(i!)\cdot (i-1)! \end{aligned} \]

再用 \(\mathcal{O}(n)\) 算出 \(inv(1)\sim inv(n)\).

\(\text{Code}\)

#include <iostream>
#include <cstdio>
#define ll long long
using namespace std;

const int MAXN = 3e6 + 5;

int n, p;
ll jc[MAXN], inv[MAXN];

ll ksm(int a, int b)
{
	ll base = a, ans = 1;
	while (b)
	{
		if (b & 1)
		{
			ans = ans * base % p;
		}
		base = base * base % p;
		b >>= 1;
	}
	return ans;
}

int main()
{
	scanf("%d%d", &n, &p);
	jc[0] = jc[1] = 1;
	for (int i = 2; i <= n; i++)
	{
		jc[i] = jc[i - 1] * i % p;
	}
	inv[n] = ksm(jc[n], p - 2);
	for (int i = n - 1; i >= 1; i--)
	{
		inv[i] = inv[i + 1] * (i + 1) % p;
	}
	for (int i = 1; i <= n; i++)
	{
		printf("%lld\n", inv[i] * jc[i - 1] % p);
	}
	return 0;
}

以上两种算法的时间复杂度均为 \(\mathcal{O}(n+\log p)\),且 \(p\) 必须为质数.

四、线性求 \(n\) 个数的逆元

P5431 【模板】乘法逆元 2

思路基本同阶乘逆元.

预处理出前缀积 \(pro_i\).

那么

\[inv(pro_{i+1})\cdot a_{i+1}=inv(pro_i) \]

然后

\[inv(a_i)=inv(pro_i)\cdot pro_{i-1} \]

时间复杂度为 \(\mathcal{O}(n+\log p)\),要卡卡常.

\(\text{Code}\)

#include <iostream>
#include <cstdio>
#define ll long long
using namespace std;

ll read()
{
	ll x = 0;
	char c = getchar();
	while (c < '0' || c > '9')
	{
		c = getchar();
	}
	while (c >= '0' && c <= '9')
	{
		x = (x << 3) + (x << 1) + c - '0';
		c = getchar();
	}
	return x;
}

const int MAXN = 5e6 + 5;

ll n, p, k;
ll a[MAXN], pro[MAXN], inv[MAXN];

ll ksm(ll a, ll b)
{
	ll base = a, ans = 1;
	while (b)
	{
		if (b & 1)
		{
			ans = ans * base % p;
		}
		base = base * base % p;
		b >>= 1;
	}
	return ans;
}

int main()
{
	n = read(), p = read(), k = read();
	pro[0] = 1;
	for (int i = 1; i <= n; i++)
	{
		a[i] = read();
		pro[i] = pro[i - 1] * a[i] % p;
	}
	inv[n] = ksm(pro[n], p - 2);
	for (int i = n - 1; i >= 1; i--)
	{
		inv[i] = inv[i + 1] * a[i + 1] % p;
	}
	ll base = k, ans = 0;
	pro[0] = 1;
	for (int i = 1; i <= n; i++)
	{
		ll x = inv[i] * pro[i - 1] % p;
		ans = (ans + base * x % p) % p;
		base = base * k % p;
	}
	printf("%lld\n", ans);
	return 0;
}
posted @ 2021-10-29 13:03  mango09  阅读(198)  评论(0编辑  收藏  举报
-->