扩展欧几里得算法 exgcd 详解

这篇文章断断续续码了好几天,尽量把式子和原理解释得很详细,基本把我理解 exgcd 脑内思路的全过程都写下来了。作者参考了此篇文章,但本文加入了更多新手小白所需的解释和细节。

前置知识:辗转相除法

如何求 gcd ⁡ ( a , b ) \gcd(a,b) gcd(a,b)?令 a ′ = b ,   b ′ = a   m o d   b a^\prime=b,\,b^\prime=a\bmod b a′=b,b′=amodb,因为余数一定小于除数, a   m o d   b < b a\bmod b<b amodb<b,所以 b ′ < b b^\prime<b b′<b,下一个 a a a 要等于这里的 b ′ < b b^\prime<b b′<b, a ,   b a,\,b a,b 都会越变越小。用上面的两个式子一直迭代下去,直到 b ′ ′ = 0 b^{\prime\prime}=0 b′′=0 的时候, a ′ ′ a^{\prime\prime} a′′ 等于 gcd ⁡ ( a , b ) \gcd(a,b) gcd(a,b),也就是我们要求的值。

为什么呢? a a a 和 b b b 一定都是 gcd ⁡ ( a , b ) \gcd(a,b) gcd(a,b) 的倍数, a ′ = b a^\prime=b a′=b,所以 a ′ a^\prime a′ 一定还是 gcd ⁡ ( a , b ) \gcd(a,b) gcd(a,b) 的倍数。而 b ′ = a   m o d   b = a − ⌊ a b ⌋ ⋅ b b^\prime=a\bmod b=a-\lfloor\frac{a}{b}\rfloor\cdot b b′=amodb=a−⌊ba⌋⋅b。 ⌊ a b ⌋ \lfloor\frac{a}{b}\rfloor ⌊ba⌋ 是个整数,一个 gcd ⁡ ( a , b ) \gcd(a,b) gcd(a,b) 的倍数减去一个 gcd ⁡ ( a , b ) \gcd(a,b) gcd(a,b) 的倍数的整数倍, b ′ b^\prime b′ 一定是 gcd ⁡ ( a , b ) \gcd(a,b) gcd(a,b) 的倍数。同理,用新的 a ′ ,   b ′ a^\prime,\,b^\prime a′,b′ 迭代出来的 a ′ ′ ,   b ′ ′ a^{\prime\prime},\,b^{\prime\prime} a′′,b′′ 也一定都是 gcd ⁡ ( a , b ) \gcd(a,b) gcd(a,b) 的倍数。所以只要一直迭代,直到 a ,   b a,\,b a,b 不能再变小,得到的一定是 gcd ⁡ ( a , b ) \gcd(a,b) gcd(a,b) 本身。

扩展欧几里得算法

这是关于 x ,   y x,\,y x,y 的二元一次方程: a x + b y = gcd ⁡ ( a , b ) ax+by=\gcd(a,b) ax+by=gcd(a,b),我们要求它的一组解。

使用相同的迭代方法,我们令 a ′ = b ,   b ′ = a   m o d   b a^\prime=b,\,b^\prime=a\bmod b a′=b,b′=amodb,得到新的方程 b x ′ + ( a   m o d   b ) y ′ = gcd ⁡ ( b , a   m o d   b ) bx^\prime+(a\bmod b)y^\prime=\gcd(b,a\bmod b) bx′+(amodb)y′=gcd(b,amodb)。这个方程和原来的方程有什么关系?根据辗转相除法,新方程右侧的 gcd ⁡ ( b , a   m o d   b ) \gcd(b,a\bmod b) gcd(b,amodb) 与原方程右侧的 gcd ⁡ ( a , b ) \gcd(a,b) gcd(a,b) 是相等的。如果能将新方程的左侧凑成 a ⋅ ( ...   ) + b ⋅ ( ...   ) a\cdot(\dots)+b\cdot(\dots) a⋅(...)+b⋅(...) 的样子,那就可以用 x ′ ,   y ′ x^\prime,\,y^\prime x′,y′ 的值倒推出 x ,   y x,\,y x,y 的值。

利用 a   m o d   b = a − ⌊ a b ⌋ ⋅ b a \bmod b=a-\lfloor\frac{a}{b}\rfloor\cdot b amodb=a−⌊ba⌋⋅b,将新方程变为 b x ′ + ( a − ⌊ a b ⌋ ⋅ b ) y ′ = gcd ⁡ ( b , a   m o d   b ) bx^\prime+(a-\lfloor\frac{a}{b}\rfloor\cdot b)y^\prime=\gcd(b,a\bmod b) bx′+(a−⌊ba⌋⋅b)y′=gcd(b,amodb)。接下来将左侧整理为我们想要的形式,右侧替换成数值相等的 gcd ⁡ ( a , b ) \gcd(a,b) gcd(a,b)。方程变为 a y ′ + b ( x ′ − ⌊ a b ⌋ y ′ ) = gcd ⁡ ( a , b ) ay^\prime+b(x^\prime-\lfloor\frac{a}{b}\rfloor y^\prime)=\gcd(a,b) ay′+b(x′−⌊ba⌋y′)=gcd(a,b)。结合原来的方程 a x + b y = gcd ⁡ ( a , b ) ax+by=\gcd(a,b) ax+by=gcd(a,b),我们可以得出: x = y ′ ,   y = x ′ − ⌊ a b ⌋ y ′ x=y^\prime,\,y=x^\prime-\lfloor\frac{a}{b}\rfloor y^\prime x=y′,y=x′−⌊ba⌋y′ 为原方程的一组解。

这有什么用?根据辗转相除法,如果一直迭代, a a a 最终会变成 gcd ⁡ ( a , b ) \gcd(a,b) gcd(a,b),而 b b b 会变成 0 0 0,也就是说我们会得到一个这样的方程 gcd ⁡ ( a , b ) ⋅ x ′ ′ + 0 ⋅ y ′ ′ = gcd ⁡ ( a , b ) \gcd(a,b)\cdot x^{\prime\prime}+0\cdot y^{\prime\prime}=\gcd(a,b) gcd(a,b)⋅x′′+0⋅y′′=gcd(a,b),这个方程瞪眼就能得到一组解 x ′ ′ = 1 ,   y ′ ′ = 0 x^{\prime\prime}=1,\,y^{\prime\prime}=0 x′′=1,y′′=0。既然最后一个方程有解了,就可以用 x = y ′ ,   y = x ′ − ⌊ a b ⌋ y ′ x=y^\prime,\,y=x^\prime-\lfloor\frac{a}{b}\rfloor y^\prime x=y′,y=x′−⌊ba⌋y′ 一直倒推,得到原方程的解。

cpp 复制代码
ll a,b;
pair<ll,ll> p,q;
pair<ll,ll> exgcd(ll aa,ll bb){
	if(bb==0){
		p.first=1,p.second=0;
		return p;
	}
	p=exgcd(bb,aa%bb);
	ll x=p.first,y=p.second;
	/*
	注意:如果你在这里没有开一个新的pair q用来存新的x,y,
	而是直接让p.first=p.second,p.second=...,
	一定要提前把p.first和p.second的值留住,不然等你改完p.first之后,
	用它改出来的p.second的值就不对了。
	*/
	q.first=y,q.second=x-aa/bb*y;
	return q;
}

要求找出一组解,使得非负整数 x x x 最小?

若 a x + b y = gcd ⁡ ( a , b ) ax+by=\gcd(a,b) ax+by=gcd(a,b) 成立,那么 a ( x − b gcd ⁡ ( a , b ) ) + b ( y + a gcd ⁡ ( a , b ) ) = gcd ⁡ ( a , b ) a(x-\frac{b}{\gcd(a,b)})+b(y+\frac{a}{\gcd(a,b)})=\gcd(a,b) a(x−gcd(a,b)b)+b(y+gcd(a,b)a)=gcd(a,b) 成立(就是在方程左侧补上两项: − a b gcd ⁡ ( a , b ) + a b gcd ⁡ ( a , b ) -\frac{ab}{\gcd(a,b)}+\frac{ab}{\gcd(a,b)} −gcd(a,b)ab+gcd(a,b)ab)。而且没有办法把 a b gcd ⁡ ( a , b ) \frac{ab}{\gcd(a,b)} gcd(a,b)ab 换成更小的数,因为要保证一加一减的时候用的那个数既是 a a a 的倍数,又是 b b b 的倍数,不然放进括号里之后 x x x 和 y y y 加上或减去的数就不是整数了。 a b gcd ⁡ ( a , b ) = lcm ⁡ ( a , b ) \frac{ab}{\gcd(a,b)}=\operatorname{lcm}(a,b) gcd(a,b)ab=lcm(a,b),是 a a a 和 b b b 共同的倍数中最小的。

所以只要先用普通的 exgcd 求出一个 x x x,最小的 x x x 就是 ( x   m o d   b gcd ⁡ ( a , b ) + b gcd ⁡ ( a , b ) )   m o d   b gcd ⁡ ( a , b ) (x\bmod\frac{b}{\gcd(a,b)}+\frac{b}{\gcd(a,b)})\bmod\frac{b}{\gcd(a,b)} (xmodgcd(a,b)b+gcd(a,b)b)modgcd(a,b)b。这也阐述了其他解的求法,就是一个加一个减,可能是向上面一样 x − b gcd ⁡ ( a , b ) ,   y + a gcd ⁡ ( a , b ) x-\frac{b}{\gcd(a,b)},\,y+\frac{a}{\gcd(a,b)} x−gcd(a,b)b,y+gcd(a,b)a,也可能是 x + b gcd ⁡ ( a , b ) ,   y − a gcd ⁡ ( a , b ) x+\frac{b}{\gcd(a,b)},\,y-\frac{a}{\gcd(a,b)} x+gcd(a,b)b,y−gcd(a,b)a。

裴蜀定理

a x + b y = c ax+by=c ax+by=c( a ,   b ,   c a,\,b,\,c a,b,c 都是整数)有整数解的充要条件是 c c c 是 gcd ⁡ ( a , b ) \gcd(a,b) gcd(a,b) 的倍数。

首先,如果 c c c 是 gcd ⁡ ( a , b ) \gcd(a,b) gcd(a,b) 的倍数,我们可以先找到 a x ′ + b y ′ = gcd ⁡ ( a , b ) a x^\prime+b y^\prime=\gcd(a,b) ax′+by′=gcd(a,b) 的一组解,设 c = k ⋅ gcd ⁡ ( a , b ) c=k\cdot \gcd(a,b) c=k⋅gcd(a,b),那么就可以两边同时乘以 k k k 得到原方程 a k x ′ + b k y ′ = c a kx^\prime+b ky^\prime=c akx′+bky′=c 的解。此时我们再解 a x + b y = c ax+by=c ax+by=c,只需要让 x = k x ′ ,   y = k y ′ x=kx^\prime,\,y=ky^\prime x=kx′,y=ky′ 就解决啦! k k k 一定是整数,所以 x ,   y x,\,y x,y 包是整数的。(充分性得证!)

如果 c c c 不是 gcd ⁡ ( a , b ) \gcd(a,b) gcd(a,b) 的倍数:将方程每项除以 gcd ⁡ ( a , b ) \gcd(a,b) gcd(a,b), a gcd ⁡ ( a , b ) ,   b gcd ⁡ ( a , b ) \frac{a}{\gcd(a,b)},\,\frac{b}{\gcd(a,b)} gcd(a,b)a,gcd(a,b)b 一定是整数,但是 c gcd ⁡ ( a , b ) \frac{c}{\gcd(a,b)} gcd(a,b)c 不是整数,所以如果 x ,   y x,\,y x,y 都是整数,就不可能满足 a gcd ⁡ ( a , b ) x + b gcd ⁡ ( a , b ) y = c gcd ⁡ ( a , b ) \frac{a}{\gcd(a,b)}x+\frac{b}{\gcd(a,b)}y=\frac{c}{\gcd(a,b)} gcd(a,b)ax+gcd(a,b)by=gcd(a,b)c,因为方程左边是整数,而右边不是。(必要性得证!)

复制代码
[check point] 你已经知道:
如何实现基本的辗转相除法求 gcd;
如何实现扩展欧几里得算法求 ax+by=gcd(a,b) 的一组解,以及用这组解得到别的解;
裴蜀定理是什么(证明只是帮助理解),以及如何求一个满足裴蜀定理的方程 ax+by=c 的解。

例题

【模板】二元一次不定方程 (exgcd)

给定不定方程: a x + b y = c ax+by=c ax+by=c

  • 若该方程无整数解,输出 − 1 -1 −1。
  • 若该方程有正整数解( x ,   y x,\,y x,y 均为正整数),则输出其正整数解的数量、所有正整数解中 x x x 的最小值、所有正整数解中 y y y 的最小值、所有正整数解中 x x x 的最大值、以及所有正整数解中 y y y 的最大值。
  • 若方程有整数解( x ,   y x,\,y x,y 均为整数,但不一定是正的),但没有正整数解,你需要输出所有整数解中 x x x 的最小正整数值, y y y 的最小正整数值。

这道题是一个很好的汇总。

第一条有没有解可以使用裴蜀定理判断, c c c 是否是 gcd ⁡ ( a , b ) \gcd(a,b) gcd(a,b) 的倍数。

确认有解了之后,就用 exgcd 算出 a x ′ + b y ′ = gcd ⁡ ( a , b ) ax^\prime+by^\prime=\gcd(a,b) ax′+by′=gcd(a,b) 的解,然后 x = x ′ ⋅ c gcd ⁡ ( a , b ) ,   y = y ′ ⋅ c gcd ⁡ ( a , b ) x=x^\prime\cdot\frac{c}{\gcd(a,b)},\,y=y^\prime\cdot\frac{c}{\gcd(a,b)} x=x′⋅gcd(a,b)c,y=y′⋅gcd(a,b)c,就得到了原方程的一组整数解。接下来可以用上面讲过的 x ,   y x,\,y x,y 一加一减的方法得到其他解。

如果此时 x ,   y x,\,y x,y 都不是正的,那么就没救了,因为 x ,   y x,\,y x,y 必需一加一减才能满足这个方程,总有一个人要减去一点,然后就更不是正的了,无正整数解。用上面讲过的取模的方法找到最小非负整数 x x x,然后特判模完之后 x = 0 x=0 x=0 的情况,将 x x x 设为 b gcd ⁡ ( a , b ) \frac{b}{\gcd(a,b)} gcd(a,b)b。 y y y 也是相似的。

如果 x ,   y x,\,y x,y 中有一个不是正的,我们先假设它是 x x x,如果是 y y y,操作是类似的。 x x x 至少需要加 k = ⌈ 1 − x b gcd ⁡ ( a , b ) ⌉ k=\lceil\frac{1-x}{\frac{b}{\gcd(a,b)}}\rceil k=⌈gcd(a,b)b1−x⌉ 个 b gcd ⁡ ( a , b ) \frac{b}{\gcd(a,b)} gcd(a,b)b 就能变成正整数,判断一下 y y y 在减完 k k k 个 a gcd ⁡ ( a , b ) \frac{a}{\gcd(a,b)} gcd(a,b)a 是否还是正整数,如果不是,就没有正整数解,用上面 x ,   y x,\,y x,y 都非正的方法分别求出 x ,   y x,\,y x,y 的最小正整数解。如果是,将 x ,   y x,\,y x,y 都变成正整数,进入下面的情况中。

现在 x ,   y x,\,y x,y 都是正整数了,题目要求的是在正整数解的范围内考虑, x ,   y x,\,y x,y 都必须是正整数才算数, x x x 的最小正整数值对应到 y y y 的最大正整数值。 x x x 可以最多减 k 1 = ⌊ x − 1 b gcd ⁡ ( a , b ) ⌋ k_1=\lfloor\frac{x-1}{\frac{b}{\gcd(a,b)}}\rfloor k1=⌊gcd(a,b)bx−1⌋ 个 b gcd ⁡ ( a , b ) \frac{b}{\gcd(a,b)} gcd(a,b)b,此时 y y y 加的值是最大的(加 k 1 k_1 k1 个 a gcd ⁡ ( a , b ) \frac{a}{\gcd(a,b)} gcd(a,b)a),肯定是正整数,得到最小的 x x x 和最大的 y y y; y y y 最多减 k 2 = ⌊ y − 1 a gcd ⁡ ( a , b ) ⌋ k_2=\lfloor\frac{y-1}{\frac{a}{\gcd(a,b)}}\rfloor k2=⌊gcd(a,b)ay−1⌋ 个 a gcd ⁡ ( a , b ) \frac{a}{\gcd(a,b)} gcd(a,b)a, x x x 也可以算出相应的值,得到最小的 y y y 和最大的 x x x。

还有一个要算正整数解的个数,就是 k 1 + k 2 + 1 k_1+k_2+1 k1+k2+1, x x x 可以最多减 k 1 k_1 k1 次,加 k 2 k_2 k2 次,还可以保持原本的值不变( 1 1 1)。

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define endl '\n'
ll t,a,b,c;
pair<ll,ll> p,q;
pair<ll,ll> exgcd(ll aa,ll bb){
	if(bb==0){
		p.first=1,p.second=0;
		return p;
	}
	p=exgcd(bb,aa%bb);
	ll x=p.first,y=p.second;
	q.first=y,q.second=x-aa/bb*y;
	return q;
}
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0);
	cin>>t;
	while(t--){
		cin>>a>>b>>c;
		ll g=__gcd(a,b);
		if(c%g){
			cout<<-1<<endl;
			continue;
		}
		p=exgcd(a,b);
		ll x=p.first*c/g,y=p.second*c/g;
		ll xm=b/g,ym=a/g;
		if(x<=0&&y<=0){
			x=(x%xm+xm)%xm;
			if(x==0) x=xm;
			y=(y%ym+ym)%ym;
			if(y==0) y=ym;
			cout<<x<<' '<<y<<endl;
		}
		else{
			if(x<=0){
				ll k=(1-x+xm-1)/xm;
				x+=k*xm,y-=k*ym;
			}
			if(y<=0){
				ll k=(1-y+ym-1)/ym;
				x-=k*xm,y+=k*ym;
			}
			if(x<=0||y<=0){
				x=(x%xm+xm)%xm;
				if(x==0) x=xm;
				y=(y%ym+ym)%ym;
				if(y==0) y=ym;
				cout<<x<<' '<<y<<endl;
			}
			else{
				ll k1=(x-1)/xm,k2=(y-1)/ym;
				cout<<k1+k2+1<<' '<<x-k1*xm<<
				' '<<y-k2*ym<<' '<<x+k2*xm<<' '<<y+k1*ym<<endl;
			}
		}
	}
	return 0;
}

同余方程

求关于 x x x 的同余方程 a x ≡ 1 ( m o d b ) a x \equiv 1 \pmod {b} ax≡1(modb) 的最小正整数解。数据保证一定有解。

这个方程的意思就是 a x ax ax 除以 b b b 余 1 1 1,说明 a x ax ax 补上几倍(可能是负数倍)的 b b b 之后就会变成 1 1 1,所以相当于 a x + b y = 1 ax+by=1 ax+by=1。原题保证了方程有解,而根据裴蜀定理, 1 1 1 必须是 gcd ⁡ ( a , b ) \gcd(a,b) gcd(a,b) 的倍数才会有解,由此可得, 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),直接用标准的 exgcd 解决即可。

还有一个最小正整数解的要求,上面已经讲过如何让 x x x 变大或者变小,只是有一个细节,上面的取模方法得到的是非负整数,所以要特判一下 x x x 变成 0 0 0 的情况,要让它变成 b gcd ⁡ ( a , b ) = b 1 = b \frac{b}{\gcd(a,b)}=\frac{b}{1}=b gcd(a,b)b=1b=b( gcd ⁡ ( a , b ) = 1 \gcd(a,b)=1 gcd(a,b)=1)。这个细节在其他题目里必须考虑,但这道题并不必需,因为这里的模数是 b b b,如果 x x x 是 b b b 的倍数,而 b y by by 也是 b b b 的倍数,那么 a x + b y ax+by ax+by 一定是 b b b 的倍数。题目保证了 b ≥ 2 b\ge 2 b≥2,所以 a x + b y ax+by ax+by 就不可能等于 1 1 1 了。此题中不会出现 x   m o d   b = 0 x\bmod b=0 xmodb=0 的情况。

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define endl '\n'
ll a,b;
pair<ll,ll> p,q;
pair<ll,ll> exgcd(ll aa,ll bb){
	if(bb==0){
		p.first=1,p.second=0;
		return p;
	}
	p=exgcd(bb,aa%bb);
	ll x=p.first,y=p.second;
	q.first=y,q.second=x-aa/bb*y;
	return q;
}
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0);
	cin>>a>>b;
	p=exgcd(a,b);
	int mod=b;
	p.first=(p.first%mod+mod)%mod;
	if(p.first==0) p.first=mod;//this line is optional
	cout<<p.first<<endl;
	return 0;
}

练习

The Equation
Red-Black Pepper
我终究还是一名 CF 刷题人

相关推荐
量子炒饭大师2 小时前
【C++11】RAII 义体加装指南 ——【包装器 与 异常】C++11中什么是包装器?有哪些包装器?C++常见异常有哪些?(附带完整代码讲解)
开发语言·c++·c++11·异常·包装器
超人小子2 小时前
小学数学巧填符号最全攻略|4 种万能解法 + 例题 + 练习题(1-6 年级通用)
数学·教育·小学教育
AI科技星2 小时前
三维网格—素数对偶性及其严格证明(全域数学·统一基态演化版)
算法·数学建模·数据挖掘
像一只黄油飞2 小时前
第二章-01-字面量
笔记·python·学习·零基础
炘爚2 小时前
深入解析内存分区:程序运行的秘密
c++
诸葛务农2 小时前
光电对抗:多模复合制导烟雾干扰外场试验及仿真(4)
人工智能·算法·光电对抗
WolfGang0073212 小时前
代码随想录算法训练营 Day39 | 动态规划 part12
算法·动态规划
zzb15802 小时前
Android Activity 与 Intent 学习笔记
android·笔记·学习
网域小星球2 小时前
C++ 从 0 入门(五)|C++ 面试必知:静态成员、友元、const 成员(高频考点)
开发语言·c++·面试·静态成员·友元函数