初等數論——同餘

hutongzhou發表於2024-04-13

前置

模運算

定義: \(a \% b (a\mod b)\) ,表示 \(a\) 除以 \(b\) 的餘數。

加法: \((a+b) \% p\)
減法: \((a-b+p) \% p\) 。加 \(p\) 是為了防止負數。
乘法: \((a \times b) \% p\)
除法無法直接運算,要用逆元(在下面會講到)。
平方運算: 快速冪

模運算滿足: 結合律,交換律,分配律

同餘的定義及其性質

定義:\(a,b\) 除以 \(m\) 的餘數相同,則稱 \(a,b\)\(m\) 同餘,記作 \(a \equiv b(\mod m)\)

由對於模n同餘的所有整陣列成的這個集合稱為同餘類(congruence class或residue class) 。

\(m\) 的同餘類一共有 \(m\) 個,它們構成 \(m\)完全剩餘系

\(1\)\(m\) 中與 \(m\) 互質的數代表的同餘類共有 \(\varphi(m)\) 個,它們構成了 \(m\) 的簡化剩餘系。

性質1: 滿足對稱性,同加性,同乘性,同冪性。

性質2:\(a \equiv b(\mod p),c \mid a,c \mid b\),則 \(a \equiv b(\mod \dfrac{m}{\gcd(m,c)})\)

費馬小定理

\(p\) 是質數,\(\forall a \in Z,a^{p} \equiv a (\mod p)\) 。當 \(a,p\) 互質時,則有 \(a^{p-1} \equiv 1(\mod p)\)

引理:當 \(p\) 是質數時,其因子只有 \(1\)\(p\) 兩個。因此,若兩個數相乘是 \(p\) 的倍數,其中必然至少有一個是 \(p\) 的倍數。
\(a\) 不是 \(p\) 的倍數時,不存在 \(x \neq y\)\(1 \leq x,y < p\) 使得 \(a \times y \equiv a \times x(\mod p)\)\(x-y<p\)\(p\) 的倍數,與 \(1 \leq x,y < p\) 的限制矛盾。
進一步地,考慮 \(1 \sim p-1\) 所有數,它們乘以 \(a\) 之後在模 \(p\) 意義下互不相同,說明仍得 \(1 \sim p-1\) 所有數。
因此,\(\prod_{i=1}^{p-1}i \equiv \prod_{i=1}^{p-1}ai(\mod p)\)。又因為 \(\prod_{i=1}^{p-1}i\)顯然不是 \(p\) 的倍數,所以費馬小定理成立。

尤拉定理

若正整數 \(a,n\) 互質,\(a^{\varphi(n)} \equiv 1(\mod n)\) 其中 \(\varphi(n)\) 為尤拉函式。

\(x_1,x_2 \cdots x_{\varphi(n)}\) 是模 \(n\) 的簡化剩餘系,那 \(ax_1,ax_2, \cdots ax_{\varphi(n)}\) 也是模 \(n\) 的簡化剩餘系。
因為 \(\gcd(a,n)=1\)\(ax_1 \dot a x_2 \cdots ax_{\varphi(n)} \equiv x_1,x_2 \cdots x_{\varphi(n)} (\mod n)\) ,所以尤拉定理成立。

可以發現費馬小定理是尤拉定理的一種特殊情況。

擴充套件尤拉定理

\(a,n\) 互質,\(a^b \equiv a^{b \mod \varphi(n)}\)

\(a,n\) 不互質,當 \(b>\varphi(n)\) 時,\(a^{b \equiv \varphi(n)+\varphi(n)}\)

因為證明有些複雜,見oi wiki

擴充套件尤拉定理可以用來降冪,當冪太大時,此公式可以減小複雜度,而當 \(b< \varphi(n)\) 冪比較小,就沒有必要降冪了。

例題:P4139 上帝與集合的正確用法

線性同餘方程

裴蜀定理

定義: \(a,b\) 是不全為零的整數,對於任意整數 \(x,y\) , $\gcd(a,b) \mid ax + by $ ,且存在整數 \(x,y\),使得 $ax +by = \gcd(a,b) $。

逆定理:\(a,b\) 是不全為零的整數,若 \(d>0\)\(a,b\) 的公約數,且存在整數 \(x,y\),使得 $ ax +by = d$,則 \(d = \gcd(a,b)\)

特殊地,設 \(a,b\) 是不全為零的整數,若存在正整數 \(x,y\) ,使得 $ax +by =1 $ ,則 \(a,b\) 互質。

當有 \(n\) 個數時,此定理依然成立。

例題: P4549 【模板】裴蜀定理

code
#include<bits/stdc++.h>
using namespace std;
int n;
int gcd(int x,int y)
{
	return y==0?x:gcd(y,x%y); 
}
int main()
{
	scanf("%d",&n);
	int x,y;
	cin>>x;
	for(int i=2;i<=n;i++)
	{
		cin>>y;
		x=gcd(x,y);
	}
	cout<<abs(x)<<endl;
	return 0;
 } 

擴充套件歐幾里得演算法

上文裴蜀定理,講述了存在整數 \(x,y\),使得 $ax +by = \gcd(a,b) $ ,這裡講述如何求解 \(x,y\)

我們設 \(d=gcd(a,b)\)。可以用輾轉相除法法求出兩個數的 $ \gcd$ ,所以我們假設它的另一個解是\(x2,y2\),滿足

\[b \times x2+(a \bmod b) \times y2=gcd(a,b) \]

也就是

\[b \times x2+(a \bmod b) \times y2=a \times x+b \times y \]

又因

\[a \mod b=a-b \times (a/b) \]

代入拆開得

\[a \times y2+(x2-y2 \times (a/b))=a \times x+b \times y \]

可得一個解

\[x=y2,y=x2-y2*(a/b) \]

現在發現只需要求出 \(x2,y2\) 而我們觀看一下求 \(x,y\) 的式子,就是輾轉相除法的式子,所以我們只需遞推求一下 \(x2,y2,x3,y3\)等。

考慮一下遞推邊界,當遞推到\(b=0\) 時,

\[a \times x+b \times y= \gcd(a,b) \]

可轉換成

\[a \times x= \gcd(a,b)=a \]

顯然 \(x=1,y \in Z\) 時 ,方程成立。

這樣就求出了方程的一組特解,我們設為 \(x_0,y_0\)

顯然不定方程 $ax +by = \gcd(a,b) $ 有無窮解,所以要求通解形式。

可以知道 $ \Delta(ax)+\Delta(by)=0$ ,設 \(\Delta =\lvert \Delta (ax) \rvert = \lvert \Delta (by) \rvert\),那 \(a,b \mid \Delta\)\(lcm(a,b) \mid \Delta\) ,所以 $ \Delta x$ 是 \(\dfrac{lcm(a,b)}{a}=\dfrac{b}{d}\) 的倍數,$ \Delta y$ 是 \(\dfrac{lcm(a,b)}{b}=\dfrac{a}{d}\) 的倍數。

所以通解為:

\[\begin{aligned} \begin{cases} x=x_0+\dfrac{b}{d} \times k\\ y=y_0-\dfrac{a}{d} \times k\\ \end{cases} \end{aligned} \]

這裡 \(k \in Z\)

實際做題時,會讓求 \(x\) 的最小正整數解,設 \(t= \dfrac{b}{d}\)

\[x=(x \mod+t) \mod t \]

特解的資料範圍:ycx 的文章 關於 exgcd 求得特解的數值範圍

線性同餘方程

定義:形如 \(a \times x\equiv c (\mod b) ,a,b \in Z\) 的方程叫做線性同餘方程

求法:

原式轉換成 :

\[a \times x + b \times y=c,y \in Z \]

這個式子和擴充套件歐幾里得演算法可以求的式子有點關聯。

\[a \times x+b \times y=gcd(a,b) \]

\(d=\gcd(a,b)\) ,由裴蜀定理可知,方程有解當 \(d \mid c\)

原式又可轉成:

\[a \times \frac{\gcd(a,b)}{c} \times x+b \times \frac{\gcd(a,b)}{c} \times y=\gcd(a,b) \]

這樣用擴充套件歐幾里得演算法就可以求出。

模板: P1082 [NOIP2012 提高組] 同餘方程

code
#include<bits/stdc++.h>
#define int long long
using namespace std;
int c,d,x,y;
int exgcd(int a,int b,int &x,int &y)
{
	if(b==0)
	{
		x=1,y=1;
		return a;
	}
	int t=exgcd(b,a%b,y,x);
	y-=(a/b)*x;
	return t;
}
signed main()
{
	scanf("%lld%lld",&c,&d);
	exgcd(c,d,x,y);
	printf("%lld",(x%d+d)%d);
	return 0;
}

線性同餘方程組(中國剩餘定理)

\[\begin{aligned} \begin{cases} x \equiv a_1 (\mod m_1)\\ x \equiv a_2 (\mod m_2)\\ \cdots \\ x \equiv a_n (\mod m_n)\\ \end{cases} \end{aligned} \]

中國剩餘定理(crt)

上式子中 \(a \in Z,m \in N^*,(a,m)=1\)

求解方法:

\(M=m_1 \times m_2 \times \cdots m_n\)

\(b_i=\dfrac{M}{m_i}\)\(b_i^{-1}\)\(b_i\)\(m_i\) 下的逆元。

\(x= \sum_{i=1}^{n} a_i \times b_i \times b_i^{-1}\)

證明:

嘗試為每個同餘方程設一個 $ x_i,x_i \equiv a_i (\mod m_i)$,可以知道 $ x_i \equiv 0(mod m_j) j \neq i$,記為條件一。

\(x_i\)\(b_i\) 的倍數,如果要滿足條件一 \(x_i=b_i\times a_i\) 即可,
那要滿足上面新設的同餘方程,讓 \(x_i \times b_i^{-1}\) 即可,如果要讓所以 \(x_i\) 合成一個 \(x\),加起來就可以了。

擴充套件中國剩餘定理(excrt)

上面沒看懂?沒關係有更好理解並適用更廣的擴充套件中國剩餘定理(excrt),它不要求模數互質。

本質就是合併方程,我們把兩個方程合併成新一個方程,依次類推就可以求解方程組了。

具體的說:合併方程 \(x \equiv a_1 (mod m_1)\)\(x \equiv a_2 (mod m_2)\)

設 $x = a_1+p \times m_1 $,所以

\[a_1 +p \times m_1 \equiv a_2 (mod m_2) \]

即:

\[p \times m_1 +q \times m_2 \equiv a_2 - a_1 (mod m_2) \]

這個地方可以用 exgcd 求解。

最後合併得到:

\[x \equiv a_1 +p \times m_1(mod (lcm(m_1,m_2))) \]

根據 裴蜀定理\(gcd(m_1,m_2)\) 不是 \(a_2-a_1\) 因數時,方程無解。

模板:

P1495 【模板】中國剩餘定理(CRT)/ 曹衝養豬

code
#include<bits/stdc++.h>
#define int long long
using namespace std;
int n;
int a[20],b[20],ans,M=1,t[20],m[20];
int x,y;
int exgcd(int a,int b)
{
	if(b==0)
	{
		x=1,y=0;
		return a;
	}
	exgcd(b,a%b);
	int t=y;
	y=x-a/b*y;
	x=t;
}
signed main()
{
	cin>>n;
	for(int i=1;i<=n;i++)
	cin>>b[i]>>a[i],M*=b[i];
	for(int i=1;i<=n;i++)
	{
		exgcd(M/b[i],b[i]);
		t[i]=x;
		t[i]=(t[i]%b[i]+b[i])%b[i];
	}
	for(int i=1;i<=n;i++)
	{
		ans+=a[i]*(M/b[i])*t[i]%M;
	}
	cout<<ans%M<<endl;
	return 0;
}

P4777 【模板】擴充套件中國剩餘定理(EXCRT)

code
#include<bits/stdc++.h>
#define int long long 
using namespace std;
const int N=1e7+10;
int n;
int m[N],X[N];
int  exgcd(int a, int b,int &x,int &y)
{
	if(b == 0) 
	{ 
		x = 1;
		y = 0;
		return a;
	} 
	int d=exgcd(b, a % b,x,y);
	long long tx = x;
	x = y;
	y = tx - a / b * y; 
	return d;
}
int mul(int a,int b,int mod)
{
	int res=0;
	while(b)
	{
		if(b&1)res=(res+a)%mod;
		a+=a;
		a%=mod;
		b>>=1;
	}
	return res;
}
signed main()
{
	int x,y,ans=0;
	scanf("%lld",&n);
	for(int i=1;i<=n;i++)
	cin>>m[i]>>X[i];
	int a1=X[1],m1=m[1];
	for(int i=2;i<=n;i++)
	{
		int a2=X[i],m2=m[i];
		int a=m1,b=m2,c=(a2-a1%m2+m2)%m2;
		int d=exgcd(a,b,x,y);
		if(c%d!=0)
		{
			puts("-1");
			return 0;
		}
		x=mul(x,(c/d),(b/d));
		ans=a1+x*m1;
		m1=m2/d*m1;
		ans=(ans%m1+m1)%m1;
		a1=ans;
	}
	cout<<ans<<endl;
	return 0;
}

乘法逆元

定義

如果一個線性同餘方程 \(ax \equiv 1 (\mod b)\)\(x\) 稱為 \(a \mod b\) 的逆元,記作 \(a^{-1}\)

應用: 上文提到除法無法直接取模,而用乘法逆元就可以求出。

根據定義可知 \(a \times a^{-1}=1\) ,那

\[a \div b=\dfrac{a \times 1}{b}= \dfrac{a \times b \times b^{-1}}{b}=a \times b^{-1} \]

求解方法

求單個數的乘法逆元

當模數 \(p\) 為質數時,可以用 費馬小定理 求解

由定義知,\(x\)\(a\) 的乘法逆元

\[ax \equiv 1 (\mod p) \]

再由費馬小定理得:

\[ax \equiv a^{p-1} (\mod p) \]

所以

\[x \equiv a^{p-2} (\mod p) \]

用快速冪就可以求出

code
int qpow(long long a, int b) {
  int ans = 1;
  a = (a % p + p) % p;
  for (; b; b >>= 1) {
    if (b & 1) ans = (a * ans) % p;
    a = (a * a) % p;
  }
  return ans;
}
int main()
{
	a=qpow(a,p-2);//求 a 的逆元
}

\(p\) 不為質數, \(\gcd(a,p)=1\) 時,可以用擴充套件歐幾里得演算法求出。(\(p\) 不為質數時也可以求)。

求乘法逆元本質上就是求線性同餘方程,用求線性同餘方程的方法求就可以了。

線性求 \(1 \cdots n\) 的逆元

用遞推來求逆元,當遞推到 \(i\) 時,設 \(k= \lfloor \dfrac{p}{i} \rfloor ,j= p \mod i\),所以

\[p=k \times i + j \]

由此又可以推出

\[k \times i + j \equiv 0 (\mod p) \]

兩邊同乘 \(i^{-1} \times j^{-1}\) 得:

\[j^{-1} \times k+i^{-1} \equiv 0(\mod p) \]

所以

\[i^{-1} \equiv -k \times j^{-1} (\mod p) \]

既然是遞推式,就要關注遞推的起始項,當 \(i=1\) 時,\(1\) 的模任何數的逆元都是 \(1\)

code
inv[1] = 1;
for (int i = 2; i <= n; ++i) {
  inv[i] = (p - p / i) * inv[p % i] % p;
}

程式碼中 \(p - p/i\) 是為了防負數。

求任意 \(n\) 個數的逆元

要求 \(a_1 \cdots a_n\) 的逆元。

定義兩個陣列 \(s,sv\)\(s_i\) 表示前 \(i\) 個數的乘積, \(sv_i\) 表示前 \(i\) 個數的逆元的乘積,那這樣 \(a_i\) 的逆元就是 \(s_{i-1} \times sv_i\)

遞推邊界 \(s_0=1\)\(sv_n=s_n\) 的逆元。

code
s[0] = 1;
for (int i = 1; i <= n; ++i) s[i] = s[i - 1] * a[i] % p;
sv[n] = qpow(s[n], p - 2);
for (int i = n; i >= 1; --i) sv[i - 1] = sv[i] * a[i] % p;
for (int i = 1; i <= n; ++i) inv[i] = sv[i] * s[i - 1] % p;

原根

相關文章