Bootstrap

中国剩余定理详解&学习笔记

前置知识:扩展欧几里得求解线性同余方程

问题

求满足 a x + b y = gcd ⁡ ( a , b ) ax+by=\gcd(a,b) ax+by=gcd(a,b) 的整数 x x x y y y

求解

d = gcd ⁡ ( a , b ) d=\gcd(a,b) d=gcd(a,b)

如果现在已经知道另一组 x 0 x_0 x0 y 0 y_0 y0 满足 b x 0 + ( a m o d    b ) y 0 = d bx_0+(a \mod b)y_0=d bx0+(amodb)y0=d (这个式子是直接根据普通欧几里得算法的辗转相除计算过程构造而来),此时将这个式子联立原方程可以得到 a x + b y = b x 0 + ( a m o d    b ) y 0 ax+by=bx_0+(a \mod b)y_0 ax+by=bx0+(amodb)y0 可以直接把取模运算转化成除法向下取整即 a m o d    b = a − b × ⌊ a b ⌋ a \mod b=a-b\times \lfloor \frac{a}{b} \rfloor amodb=ab×ba,所以有:

a x + b y = b x 0 + ( a − b × ⌊ a b ⌋ ) y 0 ax+by=bx_0+(a-b\times \lfloor \frac{a}{b} \rfloor)y_0 ax+by=bx0+(ab×ba⌋)y0

⇒ a x + b y = a y 0 + b ( x 0 − ⌊ a b ⌋ ) y 0 \Rightarrow ax+by=ay_0+b(x_0 - \lfloor \frac{a}{b}\rfloor)y_0 ax+by=ay0+b(x0ba⌋)y0

显然,上面这个方程有一组必然存在的解:

{ x = y 0 y = x 0 − ⌊ a b ⌋ y 0 \begin{cases} x=y_0\\ y=x_0-\lfloor \frac{a}{b}\rfloor y_0 \end{cases} {x=y0y=x0bay0

由于 a , b a,b a,b 已知,故只要求出一组正确的 x 0 x_0 x0 y 0 y_0 y0即可得到想求的 x x x y y y,那如何得到 x 0 x_0 x0 y 0 y_0 y0?

再次观察我们构造出的含 x 0 x_0 x0 y 0 y_0 y0 的方程和原方程:

b x 0 + ( a m o d    b ) y 0 = d bx_0+(a \mod b)y_0=d bx0+(amodb)y0=d

a x + b y = d ax+by=d ax+by=d

发现对于未知数 x x x y y y,这是一组形式上完全相同的方程,只是未知数的系数不同,所以也可以用上面的求法把 b 作为 x 0 x_0 x0 新的形式上的系数 a a a,把 a m o d    b a\mod b amodb 作为 y y y 的新的形式上的系数 b b b,即可继续递归求解。

感性理解这样做的意义,我们发现每次递归求出的新的 x 0 x_0 x0 y 0 y_0 y0 都有减小的趋势。所以最后可以通过一个固定的特解使递归终止并回溯算得最终答案。

递归过程中 a i + 1 a_{i+1} ai+1 b i + 1 b_{i+1} bi+1 不断被替代为 b i b_i bi a i m o d    b i a_i \mod b_i aimodbi,与普通欧几里得完全相同,所以最后一定会出现 a n = d a_n = d an=d b n = 0 b_n =0 bn=0,这时只需要把 x n x_n xn 1 1 1 y n y_n yn 取任意值(但为了防止回溯时越界, y n y_n yn 尽量取 0 0 0),就能满足最后一层方程 a n x n + b n y n = d a_n x_n + b_n y_n = d anxn+bnyn=d,然后回溯每层直接根据下层的 x i + 1 x_{i+1} xi+1 y i + 1 y_{i+1} yi+1 计算出当前层的 x i x_i xi y i y_i yi,不断扩大直到系数为题目中的 a a a b b b。即可得到最终的符合要求的 x x x y y y

例题

P1082 [NOIP2012 提高组] 同余方程

求满足 a x ≡ 1 ( m o d b ) ax \equiv 1 \pmod b ax1(modb) 的最小正整数解 x x x

根据同余的定义,很容易将其转化成 a x + b y = 1 ax+by=1 ax+by=1 的形式,那如何使用扩展欧几里得来转化这个式子呢?


首先简单证明: a x + b y = m ax+by=m ax+by=m 有整数解的必要条件是 gcd ⁡ ( a , b ) ∣ m \gcd(a,b)\mid m gcd(a,b)m

由最大公约数的定义, gcd ⁡ ( a , b ) ∣ a \gcd(a,b)\mid a gcd(a,b)a gcd ⁡ ( a , b ) ∣ b \gcd(a,b)\mid b gcd(a,b)b
,故 gcd ⁡ ( a , b ) ∣ a x \gcd(a,b)\mid ax gcd(a,b)ax gcd ⁡ ( a , b ) ∣ b y \gcd(a,b)\mid by gcd(a,b)by,故 gcd ⁡ ( a , b ) ∣ a x + b y = m \gcd(a,b)\mid ax+by=m gcd(a,b)ax+by=m


所以对于方程 a x + b y = 1 ax+by=1 ax+by=1 当且仅当 gcd ⁡ ( a , b ) ∣ 1 \gcd(a,b) \mid 1 gcd(a,b)1 gcd ⁡ ( a , b ) = 1 \gcd(a,b)=1 gcd(a,b)=1 时有解,故可以直接转化成扩展欧几里得的应用形式 a x + b y = gcd ⁡ ( a , b ) ax+by=\gcd(a,b) ax+by=gcd(a,b),把 x x x 用扩欧求出来即可。

但注意扩欧求的是任意一组整数解。而题目要求必须保证求出的 x x x y y y,是最小正整数解,所以需要再次转化,将 x x x 加一个数和将 y y y 减去一个数:

a ( x + k ) + b ( y − k ) = 1 a(x+k)+b(y-k)=1 a(x+k)+b(yk)=1
a ( x + k b ) + ( y − k a ) b = 1 a(x+kb)+(y-ka)b=1 a(x+kb)+(yka)b=1

注意到:

  1. 显然这并不会把方程中任何整数变成非整数。

  2. 加上或减去 b b b 不会使 x x x 错过任何解。

因此, x x x 太大只需要一直减 b b b,太小则一直加 b b b 即可。

最小正整数解可以写成:

x=(x%b+b)%b;

Code

#include<bits/stdc++.h>
using namespace std;
void exgcd(int a,int b,int &x,int &y)
{
	if(b==0){y=0,x=1;return;}
	//递归终止条件:gcd(a,b)=gcd(a,0)=a,此时有a*x+0*y=a,x取1,y取0显然有解,代值回溯 
	int x0,y0;//下一层需要求出来的x0和y0 
	exgcd(b,a%b,x0,y0);//递归求解,得到x0和y0 
	x=y0,y=x0-a/b*y0;//用x0和y0反推这一层的x和y 
} 
int main()
{
	int a,b,x,y;
	scanf("%d%d",&a,&b);
	exgcd(a,b,x,y);
	printf("%d",(x%b+b)%b);//注意此时求的x可以是负的,题目要求正整数解,用模数处理一下即可 
	return 0;
} 

运用

P1516 青蛙的约会

把题意转化为同余方程,再化成标准扩欧的形式求解,难度不大。

需要注意负数等细节的处理。

Code

#include<bits/stdc++.h>
#define LL long long
using namespace std;
LL exgcd(LL a,LL b,LL &x,LL &y)
{
	if(b==0){x=1;y=0;return a;}
	LL x0,y0;
	LL d=exgcd(b,a%b,x0,y0);//顺便求出gcd 
	x=y0,y=x0-a/b*y0;
	return d;
}
//题意转化为k(m-n)+zL=y-x:看成关于k和z的不定方程,k为所求答案 
int main()
{
	LL x,y,m,n,L,k,z,f=1;
	scanf("%lld%lld%lld%lld%lld",&x,&y,&m,&n,&L);
	if(m<n) swap(m,n),f=-1;
	//因为exgcd求的同余方程中的系数必须为正数,所以先变成正数并记录f=-1最后k要反一个符号 
	LL d=exgcd(m-n,L,k,z);//求方程k(m-n)+zL=gcd(m-n,L)的解 
	if((y-x)%d!=0)//右边不是最大公约数的倍数,特判无解的情况 
	{
		puts("Impossible");
		return 0;	
	}
	k=k*f*(y-x)/d;//在扩欧求出的方程的k基础上同时扩大为(y-x)/d倍得到原方程的k 
	k=(k%(L/d)+(L/d))%(L/d);//(L/d)相当于标准扩欧解法中的b,取模保证解为最小正整数解 
	printf("%lld",k);
	return 0;
}

中国剩余定理求解同余方程

问题

给定 n n n 组非负整数 a i , b i a_i, b_i ai,bi ,求解关于 x x x 的方程组的最小非负整数解。
{ x ≡ a 1   ( m o d   m 1 ) x ≡ a 2   ( m o d   m 2 ) . . . x ≡ a n   ( m o d   m n ) \begin{cases} x \equiv a_1\ ({\rm mod}\ m_1) \\ x\equiv a_2\ ({\rm mod}\ m_2) \\ ... \\ x \equiv a_n\ ({\rm mod}\ m_n)\end{cases} xa1 (mod m1)xa2 (mod m2)...xan (mod mn)

其中, m 1 , m 2 , … , m n m_1,m_2 , \dots ,m_n m1,m2,,mn 两两互质。

求解

首先设三个东西,分别表示所有模数的乘积 M M M、每个模数以外的其他数的乘积 M i M_i Mi M i M_i Mi 在模 m i m_i mi 意义下的逆元(易证 M i M_i Mi m i m_i mi 互质,其逆元一定存在)。

M = ∏ i = 1 n m i M=\prod_{i=1}^n m_i M=i=1nmi

M i = M m i M_i=\frac{M}{m_i} Mi=miM

t i = M i − 1 t_i=M_i^{-1} ti=Mi1

由逆元的性质,对于 ∀ i \forall i i,有:

a i t i M i ≡ a i ( m o d m i ) a_i t_i M_i \equiv a_i \pmod {m_i} aitiMiai(modmi)

对于 ∀ i , j \forall i,j i,j i ≠ j i \neq j i=j,有:

m i ∣ M j m_i \mid M_j miMj

故可以得到:

a j t j M j ≡ 0 ( m o d m i ) a_j t_j M_j \equiv 0 \pmod {m_i} ajtjMj0(modmi)

构造出通解,对于第 i i i 个同余方程,都有:

x ≡ k M + ∑ j = 1 n a j t j M j ≡ 0 + a i + ∑ j = 1 , i ≠ j n 0 ≡ a i ( m o d m i ) x \equiv kM + \sum_{j=1}^n a_j t_j M_j \equiv 0 + a_i +\sum_{j=1,i \neq j}^n 0 \equiv a_i \pmod {m_i} xkM+j=1najtjMj0+ai+j=1,i=jn0ai(modmi)

(每一个同余方程把 a i a_i ai 拿出来,其他的数都能被 m i m_i mi 整除)

故可以得到小于 M M M 的一个解:

x = ∑ j = 1 n a j t j M j   m o d   M x=\sum_{j=1}^n a_j t_j M_j \bmod M x=j=1najtjMjmodM

实际上,还需要证明这样构造出的 x x x 不漏解(即不存在其他解),这里略过证明,因为证明过程对用这个算法没啥用,我懒得写了

例题

P1495 【模板】中国剩余定理(CRT)/ 曹冲养猪

模板题,直接套用中国剩余定理即可。

Code

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=110;
int n,x,m[N],a[N],t[N],M[N],mm=1;
void exgcd(int a,int b,int &x,int &y)
{
	int x0,y0;
	if(b==0) x=1,y=0;
	else exgcd(b,a%b,x0,y0),x=y0,y=x0-a/b*y0;
}
signed main()
{
	scanf("%d",&n);
	for(int i=1;i<=n;i++) scanf("%d%d",&m[i],&a[i]),mm*=m[i];
	int y;
	for(int i=1;i<=n;i++)
	{
		M[i]=mm/m[i];
		exgcd(M[i],m[i],t[i],y);//计算M[i]在模m[i]意义下的逆元t[i] 
		t[i]=(m[i]+t[i]%m[i])%m[i];//保证t[i]为小于m[i]的正整数
		x=(x+a[i]*t[i]%mm*M[i]%mm)%mm; 
	}
	printf("%lld",x);
	return 0;
}
;