【数论】裴蜀定理与扩展欧几里得算法 (exgcd)
文章目录
- 一、裴蜀定理
- 1. 裴蜀定理的内容
- 2. 裴祖定理的推论
- 3.【模板】裴蜀定理 ⭐⭐
- 二、扩展欧几里得算法 (exgcd)
- 1. exgcd 解决的问题
- 2. exgcd 的算法过程
- 3.【模板】二元一次不定方程 (exgcd) ⭐⭐⭐⭐
- (1) 解题思路
- (2) 代码实现
- 三、exgcd 与乘法逆元
- 1. 一次同余方程
- 2. exgcd 求解一次同余方程
- 3. exgcd 求解乘法逆元
- 4. 【模板】同余方程(求逆元)⭐⭐⭐
- 四、练习:青蛙的约会 ⭐⭐⭐
- 1. 解题思路
- 2. 代码实现
一、裴蜀定理
1. 裴蜀定理的内容
裴蜀定理又称贝祖定理,它的内容是:
设 ddd 是整数 a,ba,ba,b 的最大公约数,则一定存在整数 x,yx, yx,y,使得 ax+by=dax+by=dax+by=d。
通过这个定理,我们可以判断一个不定方程是否存在整数解。
注意 a,ba, ba,b 的正负是不影响结果的。因为 a,ba, ba,b 如果存在解,那么也 a,−ba, -ba,−b 或者 −a,b-a, b−a,b 也一定存在解,只不过是在原来解的基础上添上一个负号。
2. 裴祖定理的推论
- 设 ddd 是整数 a,ba,ba,b 的最大公约数,则一定存在整数 x,yx, yx,y,使得 ax+by=ndax+by=ndax+by=nd。(n∈Zn\in Zn∈Z)
因此我们可以得出,对于一个不定方程 ax+by=cax + by = cax+by=c,如果有 gcd(a,b)∣c\gcd(a, b) \mid cgcd(a,b)∣c,那么该不定方程一定有解。
- 一定存在整数 x1,x2,⋯,xkx_1, x_2,\cdots, x_kx1,x2,⋯,xk,使得方程 a1x1+a2x2+⋯+akxk=gcd(a1,a2,⋯,ak)×na_1x_1 + a_2x_2 + \cdots + a_kx_k = \gcd(a_1, a_2, \cdots, a_k)\times na1x1+a2x2+⋯+akxk=gcd(a1,a2,⋯,ak)×n 成立。
3.【模板】裴蜀定理 ⭐⭐
【题目链接】
P4549 【模板】裴蜀定理 - 洛谷
【题目描述】
给定一个包含 nnn 个元素的整数序列 AAA,记作 A1,A2,A3,...,AnA_1,A_2,A_3,...,A_nA1,A2,A3,...,An。
求另一个包含 nnn 个元素的待定整数序列 XXX,记 S=∑i=1nAi×XiS=\sum\limits_{i=1}^nA_i\times X_iS=i=1∑nAi×Xi,使得 S>0S>0S>0 且 SSS 尽可能的小。
【输入格式】
第一行一个整数 nnn,表示序列元素个数。
第二行 nnn 个整数,表示序列 AAA。
【输出格式】
一行一个整数,表示 S>0S>0S>0 的前提下 SSS 的最小值。
【示例一】
输入
2 4059 -1782输出
99
【说明/提示】
对于 100%100\%100% 的数据,1≤n≤201 \le n \le 201≤n≤20,∣Ai∣≤105|A_i| \le 10^5∣Ai∣≤105,且 AAA 序列不全为 000。
根据裴蜀定理, 不定方程 S∑i=1nAi×XiS\sum\limits_{i=1}^nA_i\times X_iSi=1∑nAi×Xi 的解为 gcd(A1,A2,⋯,An)×k\gcd(A_1, A_2, \cdots, A_n)\times kgcd(A1,A2,⋯,An)×k,要使得它的解大于零且尽可能的小,那么实际上就是求 gcd(A1,A2,⋯,An)\gcd(A_1, A_2, \cdots, A_n)gcd(A1,A2,⋯,An)。
#include<iostream>using namespace std;// 欧几里得算法求最大公因数
int gcd(int a, int b)
{return b == 0 ? a : gcd(b, a % b);
}int main()
{int n; cin >> n;int ret; cin >> ret;while(--n){int x; cin >> x;ret = gcd(ret, abs(x));}cout << ret << endl;return 0;
}
二、扩展欧几里得算法 (exgcd)
1. exgcd 解决的问题
对于一个不定方程
ax+by=gcd(a,b)ax+by=\gcd(a,b) ax+by=gcd(a,b)
来说,由裴祖定理我们知道这个方程一定是有解的,那么我们去求一组非零整数解 x,yx,yx,y 的过程,就要用到扩展欧几里得算法。
2. exgcd 的算法过程
它的计算过程如下:
注:下面推导中得分式除法表示整除。
- 当 b=0b = 0b=0 时,
ax+by=axax + by = axax+by=ax,因此有一组解为 x=1,y=0x = 1, y = 0x=1,y=0;
- 当 b≠0b \ne 0b=0 时,
由欧几里得算法得:gcd(a,b)=gcd(b,amodb)\gcd(a, b) = \gcd(b, a \bmod b)gcd(a,b)=gcd(b,amodb),则
gcd(a,b)=ax+by⇓gcd(b,amodb)=bx1+(amodb)y1=bx1+(a−ab×b)y1=ay1+b(x1−aby1)\gcd(a,b) = ax+by \\ \Downarrow \\ \begin{aligned} \gcd(b, a \bmod b) &= bx_1 + (a \bmod b)y_1 \\ &=bx_1 + (a - \frac{a}{b}\times b)y_1 \\ &= ay_1 + b(x_1 - \frac{a}{b}y_1) \end{aligned} gcd(a,b)=ax+by⇓gcd(b,amodb)=bx1+(amodb)y1=bx1+(a−ba×b)y1=ay1+b(x1−bay1)
等式左右应该相等,因此
x=y1,y=x1−aby1x = y1, y = x1 - \frac{a}{b}y_1 x=y1,y=x1−bay1
于是我们可以利用递归,先求出下一层得 x1,y1x_1, y_1x1,y1,再求出当前的 x,yx, yx,y。上述递归过程,可以求出一组特解:x0,y0x_0, y_0x0,y0,之后我们可以构造出通解,通解为
{x=x0+bgcd(a,b)×ky=y0−agcd(a,b)×k\begin{cases} x = x_0 + \frac{b}{\gcd(a, b)}\times k \\ y = y_0 - \frac{a}{\gcd(a, b)}\times k \end{cases} {x=x0+gcd(a,b)b×ky=y0−gcd(a,b)a×k
其中 k∈N+k\in N^+k∈N+。
代码实现:
typedef long long LL;// exgcd(扩展欧几里得算法) 的返回值和 gcd(欧几里得算法) 一样,都是返回 gcd(a, b)
// 不同的是 exgcd 还可以求出不定方程 ax + by = gcd(a, b) 的一组特解 x, y
// 由于 C++ 只能有一个返回值,所以我们要求的 x, y 需要传引用
LL exgcd(LL a, LL b, LL& x, LL& y)
{if(b == 0){x = 1, y = 0;return a;}// b 不为 0LL x1, y1, d;// 递归求出 x1, y1d = exgcd(b, a % b, x1, y1);// 利用公式求出当前层的 x, yx = y1;y = x1 - a / b * y1;return d;
}
时间复杂度与欧几里得算法一致,为 O(logn)O(\log n)O(logn)。
如果自己手算一个不定方程 ax+by=gcd(a,b)ax+by=\gcd(a,b)ax+by=gcd(a,b) 的解,可以利用先写出欧几里得算法的过程,之后逆向推出 xxx 和 yyy,下面给出一个示例:

3.【模板】二元一次不定方程 (exgcd) ⭐⭐⭐⭐
【题目链接】
P5656 【模板】二元一次不定方程 (exgcd) - 洛谷
【题目描述】
给定不定方程
ax+by=cax+by=cax+by=c
若该方程无整数解,输出 −1-1−1。
若该方程有整数解,且有正整数解,则输出其正整数解的数量,所有正整数解中 xxx 的最小值,所有正整数解中 yyy 的最小值,所有正整数解中 xxx 的最大值,以及所有正整数解中 yyy 的最大值。
若方程有整数解,但没有正整数解,你需要输出所有整数解中 xxx 的最小正整数值, yyy 的最小正整数值。正整数解即为 x,yx, yx,y 均为正整数的解,0\boldsymbol{0}0 不是正整数。
整数解即为 x,yx,yx,y 均为整数的解。
xxx 的最小正整数值即所有 xxx 为正整数的整数解中 xxx 的最小值,yyy 同理。
【输入格式】
第一行一个正整数 TTT,代表数据组数。
接下来 TTT 行,每行三个由空格隔开的正整数 a,b,ca, b, ca,b,c。
【输出格式】
TTT 行。
若该行对应的询问无整数解,一个数字 −1-1−1。
若该行对应的询问有整数解但无正整数解,包含 222 个由空格隔开的数字,依次代表整数解中,xxx 的最小正整数值,yyy 的最小正整数值。
否则包含 555 个由空格隔开的数字,依次代表正整数解的数量,正整数解中,xxx 的最小值,yyy 的最小值,xxx 的最大值,yyy 的最大值。读入输出量较大,注意使用较快的读入输出方式
【示例一】
输入
7 2 11 100 3 18 6 192 608 17 19 2 60817 11 45 14 19 19 810 98 76 5432输出
4 6 2 39 8 2 1 -1 1600 1 18 3199 30399 34 3 -1 2 12 7 50 56
【说明/提示】
对于 100%100\%100% 的数据,1≤T≤2×1051 \le T \le 2 \times {10}^51≤T≤2×105,1≤a,b,c≤1091 \le a, b, c \le {10}^91≤a,b,c≤109。
(1) 解题思路
求解一个二元一次不定方程 ax+by=cax + by = cax+by=c 的通解:
先利用扩展欧几里得算法求出 ax+by=gcd(a,b)ax + by = \gcd(a, b)ax+by=gcd(a,b) 的一组特解 x0,y0x_0, y_0x0,y0,以及 d=gcd(a,b)d = \gcd(a, b)d=gcd(a,b);
用裴蜀定理判断原方程是否有解:
- 如果 d∤cd \nmid cd∤c,则无解;
- 如果 d∣cd\mid cd∣c,则有解。
在有解的前提下:
- 原方程的一组特解为
x1=x0×cdy1=y0×cdx_1 = \frac{x_0\times c}{d}\\ y_1 = \frac{y_0\times c}{d} x1=dx0×cy1=dy0×c
- 通解为
x=x1+bgcd(a,b)×k,y=y1−agcd(a,b)×kx = x_1 + \frac{b}{\gcd(a, b)}\times k,\\y = y_1 - \frac{a}{\gcd(a, b)}\times k x=x1+gcd(a,b)b×k,y=y1−gcd(a,b)a×k
当我们求出特解之后,由通解的形式不难发现,当 xxx 在增大的同时,yyy 在减小,所以当 xxx 为最小正整数的时候,此时如果 yyy 不是正整数,那么 x,yx, yx,y 永远不可能同时为正整数,即没有正整数解。那么我们先利用 “模加模” 求出 xxx 的最小正整数解,
// 特解为 x, gcd(a, b) = d
int k1 = b / d,
x = (x % k1 + k1) % k1; // 把 x 补成最小正整数
之后利用原方程 ax+by=cax + by = cax+by=c 反解出 yyy,即 y=(c−ax)/by = (c - ax)/by=(c−ax)/b,判断 yyy 是否也为正整数即可判断原方程是否有正整数解。
(2) 代码实现
#include<iostream>using namespace std;typedef long long LL;// 扩展欧几里得求 ax + by = gcd(a, b) 的特解
LL exgcd(LL a, LL b, LL& x, LL& y)
{if(b == 0){x = 1, y = 0;return a;}LL x1, y1, d;d = exgcd(b, a % b, x1, y1);x = y1;y = x1 - a / b * y1;return d;
}int main()
{int t;scanf("%d", &t);LL a, b, c;while(t--){scanf("%lld %lld %lld", &a, &b, &c);LL x, y;LL d = exgcd(a, b, x, y);// 如果 d 不能整除 c, 则方程无解,输出-1if(c % d){printf("-1\n");continue;}// 求出 ax + by = c 的特解x = c / d * x;y = c / d * y;LL k1 = b / d, k2 = a / d;x = (x % k1 + k1) % k1; // 把 x 补成最小正整数x = x == 0 ? k1 : x; // 注意此时 x 有可能为 0, 不是正整数,至少要为 k1y = (c - a * x) / b; // 根据原方程反解出 yLL max_x, max_y, min_x, min_y;if(y > 0) // 有整数解且有正整数解{min_x = x, max_y = y;y %= k2; // 把 y 求成最小正整数解y = y == 0 ? k2 : y;min_y = y;max_x = (c - b * y) / a;// 解的个数就是 x(或y) 的最大正整数解 - 最小正整数解 / k1(或k2) + 1printf("%lld %lld %lld %lld %lld\n", (max_x - min_x) / k1 + 1, min_x, min_y, max_x, max_y);}else // 有整数解但没有正整数解{min_x = x;y = (y % k2 + k2) % k2;y = y == 0 ? k2 : y;min_y = y;printf("%lld %lld\n", min_x, min_y);}}return 0;
}
三、exgcd 与乘法逆元
1. 一次同余方程
形如
ax≡b(modm)ax ≡ b(\bmod m) ax≡b(modm)
的一个方程就是同余方程,由于 xxx 的最高次数为 111,所以这是一个一次同余方程。
2. exgcd 求解一次同余方程
一个 ax≡b(modm)ax ≡ b(\bmod m)ax≡b(modm) 这样的同余方程可以转换为 ax=ym+bax = ym + bax=ym+b,移项得 ax−my=bax - my = bax−my=b;
由于正负号不影响结果,所以等价于求解 ax+my=bax + my = bax+my=b 这一个不定方程;
由裴蜀定理,当 gcd(a,m)∣b\gcd(a, m)\mid bgcd(a,m)∣b 时,该方程有解;
所以,我们可以通过扩展欧几里得算法求出 ax+my=gcd(a,m)ax + my = \gcd(a, m)ax+my=gcd(a,m) 得特解,进而判断是否有 gcd(a,m)∣b\gcd(a, m)\mid bgcd(a,m)∣b,如果成立,那么说明 ax+my=bax + my = bax+my=b 有解,设用 exgcd 求得的 ax+my=gcd(a,m)ax + my = \gcd(a, m)ax+my=gcd(a,m) 的特解为 x0,y0x_0, y_0x0,y0,则原方程的特解为
x1=x0×bgcd(a,m)y1=y0×bgcd(a,m)x_1 = \frac{x_0\times b}{\gcd(a, m)}\\ y_1 = \frac{y_0\times b}{\gcd(a, m)} x1=gcd(a,m)x0×by1=gcd(a,m)y0×b
进而可以求出通解。
3. exgcd 求解乘法逆元
对于正整数 aaa 和 ppp,若有 ax≡1(modp)ax\equiv1\pmod pax≡1(modp),那么把这个同余方程中的 xxx 的解叫做 aaa 模 ppp 的乘法逆元,简称逆元。
逆元可以通过费马小定理求,但是必须要求 ppp 是一个质数,但是如果 ppp 不是质数,那么我们可以用扩展欧几里得算法求解乘法逆元,不难发现,这本质上就是求解一个同余方程的过程。
原同余方程可以转化为 ax+py=1ax + py = 1ax+py=1 的形式,根据裴蜀定理,该不定方程要有解,必须满足 gcd(a,p)∣1\gcd(a, p) \mid 1gcd(a,p)∣1,那么 gcd(a,p)\gcd(a, p)gcd(a,p) 必须等于 111,也就是说 a,pa, pa,p 必须互质。所以能用扩展欧几里得求解乘法逆元的条件就是 a,pa, pa,p 互质。
4. 【模板】同余方程(求逆元)⭐⭐⭐
【题目链接】
【模板】同余方程

#include<iostream>using namespace std;typedef long long LL;// 扩展欧几里得
LL exgcd(LL a, LL b, LL& x, LL& y)
{if(b == 0){x = 1, y = 0;return a;}LL x1, y1, d;d = exgcd(b, a % b, x1, y1);x = y1;y = x1 - a / b * y1;return d;
}int main()
{int t;cin >> t;while(t--){LL a, b;cin >> a >> b;// ax + by = 1LL x, y;int d = exgcd(a, b, x, y);// 如果 a, p 不互质,那么一定没有解if(d != 1) cout << -1 << endl;else{LL k = b / d;x = (x % k + k) % k; // 补成最小正整数解x = x == 0 ? k : x;cout << x << endl;}}return 0;
}
四、练习:青蛙的约会 ⭐⭐⭐
【题目链接】
P1516 青蛙的约会 - 洛谷
【题目描述】
两只青蛙在网上相识了,它们聊得很开心,于是觉得很有必要见一面。它们很高兴地发现它们住在同一条纬度线上,于是它们约定各自朝西跳,直到碰面为止。可是它们出发之前忘记了一件很重要的事情,既没有问清楚对方的特征,也没有约定见面的具体位置。不过青蛙们都是很乐观的,它们觉得只要一直朝着某个方向跳下去,总能碰到对方的。但是除非这两只青蛙在同一时间跳到同一点上,不然是永远都不可能碰面的。为了帮助这两只乐观的青蛙,你被要求写一个程序来判断这两只青蛙是否能够碰面,会在什么时候碰面。
我们把这两只青蛙分别叫做青蛙 A 和青蛙 B,并且规定纬度线上东经 000 度处为原点,由东往西为正方向,单位长度 111 米,这样我们就得到了一条首尾相接的数轴。设青蛙 A 的出发点坐标是 xxx,青蛙 B 的出发点坐标是 yyy。青蛙 A 一次能跳 mmm 米,青蛙 B 一次能跳 nnn 米,两只青蛙跳一次所花费的时间相同。纬度线总长 LLL 米。现在要你求出它们跳了几次以后才会碰面。
【输入格式】
输入只包括一行五个整数 x,y,m,n,Lx,y,m,n,Lx,y,m,n,L。
【输出格式】
输出碰面所需要的次数,如果永远不可能碰面则输出一行一个字符串
Impossible。
【示例一】
输入
1 2 3 4 5输出
4
【说明/提示】
对于 100%100\%100% 的数据,1≤x,y,m,n≤2×1091 \le x, y, m, n \le 2 \times 10^91≤x,y,m,n≤2×109,x≠yx \ne yx=y,1≤L≤2.1×1091 \le L \le 2.1 \times 10^91≤L≤2.1×109。
1. 解题思路
设两个青蛙相遇时跳了 ttt 次,那么相对于原点来说,青蛙 A 跳的总路程就是 x+tmx + tmx+tm,青蛙 B 跳的总路程是 y+tny + tny+tn,且这两个总路程的差值刚好是纬度线总长度的 kkk 倍,即
x+tm−(y+tn)=klx + tm - (y + tn) = kl x+tm−(y+tn)=kl
整理后得到
(m−n)t−kl=y−x(m - n)t - kl = y - x (m−n)t−kl=y−x
这个式子其实就是一个不定方程 ax+by=cax + by = cax+by=c,我们要求的 ttt 就是这里的 xxx。
上面的式子还可以看作一个同余方程,如下
(m−n)t≡y−x(modl)(m - n)t\equiv y - x\pmod l (m−n)t≡y−x(modl)
如果 m−n<0m - n < 0m−n<0 那么等价于求解
(n−m)t≡x−y(modl)(n - m)t\equiv x - y\pmod l (n−m)t≡x−y(modl)
2. 代码实现
#include<iostream>using namespace std;typedef long long LL;LL exgcd(LL a, LL b, LL& x, LL& y)
{if(b == 0){x = 1, y = 0;return a;}LL x1, y1, d;d = exgcd(b, a % b, x1, y1);x = y1;y = x1 - a / b * y1;return d;
}int main()
{LL x, y, m, n, l;cin >> x >> y >> m >> n >> l;// 设跳 x0 次LL a = m - n, b = l, c = y - x, x0, y0;if(a < 0) // 处理负数{a = -a;c = -c;}// a * x0 + b * y0 = cLL d = exgcd(a, b, x0, y0);if(c % d) cout << "Impossible" << endl;else{x0 = c / d * x0;LL k = b / d;x0 = (x0 % k + k) % k;cout << x0 << endl;}return 0;
}
