【数论】裴蜀定理与扩展欧几里得算法 (exgcd)

文章目录

  • 一、裴蜀定理
    • [1. 裴蜀定理的内容](#1. 裴蜀定理的内容)
    • [2. 裴祖定理的推论](#2. 裴祖定理的推论)
    • [3.【模板】裴蜀定理 ⭐⭐](#3.【模板】裴蜀定理 ⭐⭐)
  • 二、扩展欧几里得算法 (exgcd)
    • [1. exgcd 解决的问题](#1. exgcd 解决的问题)
    • [2. exgcd 的算法过程](#2. exgcd 的算法过程)
    • [3.【模板】二元一次不定方程 (exgcd) ⭐⭐⭐⭐](#3.【模板】二元一次不定方程 (exgcd) ⭐⭐⭐⭐)
      • [(1) 解题思路](#(1) 解题思路)
      • [(2) 代码实现](#(2) 代码实现)
  • [三、exgcd 与乘法逆元](#三、exgcd 与乘法逆元)
    • [1. 一次同余方程](#1. 一次同余方程)
    • [2. exgcd 求解一次同余方程](#2. exgcd 求解一次同余方程)
    • [3. exgcd 求解乘法逆元](#3. exgcd 求解乘法逆元)
    • [4. 【模板】同余方程(求逆元)⭐⭐⭐](#4. 【模板】同余方程(求逆元)⭐⭐⭐)
  • [四、练习:青蛙的约会 ⭐⭐⭐](#四、练习:青蛙的约会 ⭐⭐⭐)
    • [1. 解题思路](#1. 解题思路)
    • [2. 代码实现](#2. 代码实现)

一、裴蜀定理

1. 裴蜀定理的内容

裴蜀定理 又称贝祖定理,它的内容是:

设 d d d 是整数 a , b a,b a,b 的最大公约数,则一定存在整数 x , y x, y x,y,使得 a x + b y = d ax+by=d ax+by=d。

通过这个定理,我们可以判断一个不定方程是否存在整数解

注意 a , b a, b a,b 的正负是不影响结果的。因为 a , b a, b a,b 如果存在解,那么也 a , − b a, -b a,−b 或者 − a , b -a, b −a,b 也一定存在解,只不过是在原来解的基础上添上一个负号。


2. 裴祖定理的推论

  • 设 d d d 是整数 a , b a,b a,b 的最大公约数,则一定存在整数 x , y x, y x,y,使得 a x + b y = n d ax+by=nd ax+by=nd。( n ∈ Z n\in Z n∈Z)

因此我们可以得出,对于一个不定方程 a x + b y = c ax + by = c ax+by=c,如果有 gcd ⁡ ( a , b ) ∣ c \gcd(a, b) \mid c gcd(a,b)∣c,那么该不定方程一定有解。

  • 一定存在整数 x 1 , x 2 , ⋯   , x k x_1, x_2,\cdots, x_k x1,x2,⋯,xk,使得方程 a 1 x 1 + a 2 x 2 + ⋯ + a k x k = gcd ⁡ ( a 1 , a 2 , ⋯   , a k ) × n a_1x_1 + a_2x_2 + \cdots + a_kx_k = \gcd(a_1, a_2, \cdots, a_k)\times n a1x1+a2x2+⋯+akxk=gcd(a1,a2,⋯,ak)×n 成立。

3.【模板】裴蜀定理 ⭐⭐

【题目链接】

P4549 【模板】裴蜀定理 - 洛谷

【题目描述】

给定一个包含 n n n 个元素的整数 序列 A A A,记作 A 1 , A 2 , A 3 , . . . , A n A_1,A_2,A_3,...,A_n A1,A2,A3,...,An。

求另一个包含 n n n 个元素的待定整数 序列 X X X,记 S = ∑ i = 1 n A i × X i S=\sum\limits_{i=1}^nA_i\times X_i S=i=1∑nAi×Xi,使得 S > 0 S>0 S>0 且 S S S 尽可能的小。

【输入格式】

第一行一个整数 n n n,表示序列元素个数。

第二行 n n n 个整数,表示序列 A A A。

【输出格式】

一行一个整数,表示 S > 0 S>0 S>0 的前提下 S S S 的最小值。

【示例一】

输入

复制代码
2
4059 -1782

输出

复制代码
99

【说明/提示】

对于 100 % 100\% 100% 的数据, 1 ≤ n ≤ 20 1 \le n \le 20 1≤n≤20, ∣ A i ∣ ≤ 1 0 5 |A_i| \le 10^5 ∣Ai∣≤105,且 A A A 序列不全为 0 0 0。


根据裴蜀定理, 不定方程 S ∑ i = 1 n A i × X i S\sum\limits_{i=1}^nA_i\times X_i Si=1∑nAi×Xi 的解为 gcd ⁡ ( A 1 , A 2 , ⋯   , A n ) × k \gcd(A_1, A_2, \cdots, A_n)\times k gcd(A1,A2,⋯,An)×k,要使得它的解大于零且尽可能的小,那么实际上就是求 gcd ⁡ ( A 1 , A 2 , ⋯   , A n ) \gcd(A_1, A_2, \cdots, A_n) gcd(A1,A2,⋯,An)。

cpp 复制代码
#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 解决的问题

对于一个不定方程
a x + b y = gcd ⁡ ( a , b ) ax+by=\gcd(a,b) ax+by=gcd(a,b)

来说,由裴祖定理我们知道这个方程一定是有解的,那么我们去求一组非零整数解 x , y x,y x,y 的过程,就要用到扩展欧几里得算法


2. exgcd 的算法过程

它的计算过程如下:

注:下面推导中得分式除法表示整除。

  • 当 b = 0 b = 0 b=0 时,

a x + b y = a x ax + by = ax ax+by=ax,因此有一组解为 x = 1 , y = 0 x = 1, y = 0 x=1,y=0;

  • 当 b ≠ 0 b \ne 0 b=0 时,

由欧几里得算法得: gcd ⁡ ( a , b ) = gcd ⁡ ( b , a   m o d   b ) \gcd(a, b) = \gcd(b, a \bmod b) gcd(a,b)=gcd(b,amodb),则
gcd ⁡ ( a , b ) = a x + b y ⇓ gcd ⁡ ( b , a   m o d   b ) = b x 1 + ( a   m o d   b ) y 1 = b x 1 + ( a − a b × b ) y 1 = a y 1 + b ( x 1 − a b y 1 ) \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 = y 1 , y = x 1 − a b y 1 x = y1, y = x1 - \frac{a}{b}y_1 x=y1,y=x1−bay1

于是我们可以利用递归,先求出下一层得 x 1 , y 1 x_1, y_1 x1,y1,再求出当前的 x , y x, y x,y。

上述递归过程,可以求出一组特解: x 0 , y 0 x_0, y_0 x0,y0,之后我们可以构造出通解,通解为
{ x = x 0 + b gcd ⁡ ( a , b ) × k y = y 0 − a gcd ⁡ ( 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+。

代码实现:

cpp 复制代码
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 不为 0
    LL x1, y1, d;
    // 递归求出 x1, y1
    d = exgcd(b, a % b, x1, y1);
    // 利用公式求出当前层的 x, y
    x = y1;
    y = x1 - a / b * y1;
    
    return d;
}

时间复杂度与欧几里得算法一致,为 O ( log ⁡ n ) O(\log n) O(logn)。

如果自己手算一个不定方程 a x + b y = gcd ⁡ ( a , b ) ax+by=\gcd(a,b) ax+by=gcd(a,b) 的解,可以利用先写出欧几里得算法的过程,之后逆向推出 x x x 和 y y y,下面给出一个示例:


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

【题目链接】

P5656 【模板】二元一次不定方程 (exgcd) - 洛谷

【题目描述】

给定不定方程

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

若该方程无整数解,输出 − 1 -1 −1。

若该方程有整数解,且有正整数解,则输出其正整数 解的数量,所有正整数 解中 x x x 的最小值,所有正整数 解中 y y y 的最小值,所有正整数 解中 x x x 的最大值,以及所有正整数 解中 y y y 的最大值。

若方程有整数解,但没有正整数解,你需要输出所有整数解 中 x x x 的最小正整数值, y y y 的最小正整数值。

正整数解即为 x , y x, y x,y 均为正整数的解, 0 \boldsymbol{0} 0 不是正整数

整数解即为 x , y x,y x,y 均为整数的解。
x x x 的最小正整数值即所有 x x x 为正整数的整数解中 x x x 的最小值, y y y 同理。

【输入格式】

第一行一个正整数 T T T,代表数据组数。

接下来 T T T 行,每行三个由空格隔开的正整数 a , b , c a, b, c a,b,c。

【输出格式】

T T T 行。

若该行对应的询问无整数解,一个数字 − 1 -1 −1。

若该行对应的询问有整数解但无正整数解,包含 2 2 2 个由空格隔开的数字,依次代表整数解中, x x x 的最小正整数值, y y y 的最小正整数值。

否则包含 5 5 5 个由空格隔开的数字,依次代表正整数解的数量,正整数解中, x x x 的最小值, y y y 的最小值, x x x 的最大值, y y y 的最大值。

读入输出量较大,注意使用较快的读入输出方式

【示例一】

输入

复制代码
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 × 10 5 1 \le T \le 2 \times {10}^5 1≤T≤2×105, 1 ≤ a , b , c ≤ 10 9 1 \le a, b, c \le {10}^9 1≤a,b,c≤109。


(1) 解题思路

求解一个二元一次不定方程 a x + b y = c ax + by = c ax+by=c 的通解:

  1. 先利用扩展欧几里得算法求出 a x + b y = gcd ⁡ ( a , b ) ax + by = \gcd(a, b) ax+by=gcd(a,b) 的一组特解 x 0 , y 0 x_0, y_0 x0,y0,以及 d = gcd ⁡ ( a , b ) d = \gcd(a, b) d=gcd(a,b);

  2. 用裴蜀定理判断原方程是否有解:

    • 如果 d ∤ c d \nmid c d∤c,则无解;
    • 如果 d ∣ c d\mid c d∣c,则有解。
  3. 在有解的前提下:

    • 原方程的一组特解为

    x 1 = x 0 × c d y 1 = y 0 × c d x_1 = \frac{x_0\times c}{d}\\ y_1 = \frac{y_0\times c}{d} x1=dx0×cy1=dy0×c

    • 通解为

x = x 1 + b gcd ⁡ ( a , b ) × k , y = y 1 − a gcd ⁡ ( a , b ) × k x = 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

当我们求出特解之后,由通解的形式不难发现,当 x x x 在增大的同时, y y y 在减小,所以当 x x x 为最小正整数的时候,此时如果 y y y 不是正整数,那么 x , y x, y x,y 永远不可能同时为正整数,即没有正整数解。那么我们先利用 "模加模" 求出 x x x 的最小正整数解,

cpp 复制代码
// 特解为 x, gcd(a, b) = d
int k1 = b / d,
x = (x % k1 + k1) % k1; // 把 x 补成最小正整数

之后利用原方程 a x + b y = c ax + by = c ax+by=c 反解出 y y y,即 y = ( c − a x ) / b y = (c - ax)/b y=(c−ax)/b,判断 y y y 是否也为正整数即可判断原方程是否有正整数解。


(2) 代码实现

cpp 复制代码
#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, 则方程无解,输出-1
        if(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, 不是正整数,至少要为 k1
        y = (c - a * x) / b;  // 根据原方程反解出 y

        LL 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) + 1
            printf("%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. 一次同余方程

形如
a x ≡ b (   m o d   m ) ax ≡ b(\bmod m) ax≡b(modm)

的一个方程就是同余方程,由于 x x x 的最高次数为 1 1 1,所以这是一个一次同余方程。


2. exgcd 求解一次同余方程

一个 a x ≡ b (   m o d   m ) ax ≡ b(\bmod m) ax≡b(modm) 这样的同余方程可以转换为 a x = y m + b ax = ym + b ax=ym+b,移项得 a x − m y = b ax - my = b ax−my=b;

由于正负号不影响结果,所以等价于求解 a x + m y = b ax + my = b ax+my=b 这一个不定方程;

由裴蜀定理,当 gcd ⁡ ( a , m ) ∣ b \gcd(a, m)\mid b gcd(a,m)∣b 时,该方程有解;

所以,我们可以通过扩展欧几里得算法求出 a x + m y = gcd ⁡ ( a , m ) ax + my = \gcd(a, m) ax+my=gcd(a,m) 得特解,进而判断是否有 gcd ⁡ ( a , m ) ∣ b \gcd(a, m)\mid b gcd(a,m)∣b,如果成立,那么说明 a x + m y = b ax + my = b ax+my=b 有解,设用 exgcd 求得的 a x + m y = gcd ⁡ ( a , m ) ax + my = \gcd(a, m) ax+my=gcd(a,m) 的特解为 x 0 , y 0 x_0, y_0 x0,y0,则原方程的特解为
x 1 = x 0 × b gcd ⁡ ( a , m ) y 1 = y 0 × b gcd ⁡ ( 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 求解乘法逆元

对于正整数 a a a 和 p p p,若有 a x ≡ 1 ( m o d p ) ax\equiv1\pmod p ax≡1(modp),那么把这个同余方程中的 x x x 的解叫做 a a a 模 p p p 的乘法逆元,简称逆元。

逆元可以通过费马小定理求,但是必须要求 p p p 是一个质数,但是如果 p p p 不是质数,那么我们可以用扩展欧几里得算法求解乘法逆元,不难发现,这本质上就是求解一个同余方程的过程。

原同余方程可以转化为 a x + p y = 1 ax + py = 1 ax+py=1 的形式,根据裴蜀定理,该不定方程要有解,必须满足 gcd ⁡ ( a , p ) ∣ 1 \gcd(a, p) \mid 1 gcd(a,p)∣1,那么 gcd ⁡ ( a , p ) \gcd(a, p) gcd(a,p) 必须等于 1 1 1,也就是说 a , p a, p a,p 必须互质。所以能用扩展欧几里得求解乘法逆元的条件就是 a , p a, p a,p 互质。


4. 【模板】同余方程(求逆元)⭐⭐⭐

【题目链接】

【模板】同余方程


cpp 复制代码
#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 = 1
        LL 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,并且规定纬度线上东经 0 0 0 度处为原点,由东往西为正方向,单位长度 1 1 1 米,这样我们就得到了一条首尾相接的数轴。设青蛙 A 的出发点坐标是 x x x,青蛙 B 的出发点坐标是 y y y。青蛙 A 一次能跳 m m m 米,青蛙 B 一次能跳 n n n 米,两只青蛙跳一次所花费的时间相同。纬度线总长 L L L 米。现在要你求出它们跳了几次以后才会碰面。

【输入格式】

输入只包括一行五个整数 x , y , m , n , L x,y,m,n,L x,y,m,n,L。

【输出格式】

输出碰面所需要的次数,如果永远不可能碰面则输出一行一个字符串 Impossible

【示例一】

输入

复制代码
1 2 3 4 5

输出

复制代码
4

【说明/提示】

对于 100 % 100\% 100% 的数据, 1 ≤ x , y , m , n ≤ 2 × 1 0 9 1 \le x, y, m, n \le 2 \times 10^9 1≤x,y,m,n≤2×109, x ≠ y x \ne y x=y, 1 ≤ L ≤ 2.1 × 1 0 9 1 \le L \le 2.1 \times 10^9 1≤L≤2.1×109。


1. 解题思路

设两个青蛙相遇时跳了 t t t 次,那么相对于原点来说,青蛙 A 跳的总路程就是 x + t m x + tm x+tm,青蛙 B 跳的总路程是 y + t n y + tn y+tn,且这两个总路程的差值刚好是纬度线总长度的 k k k 倍,即
x + t m − ( y + t n ) = k l x + tm - (y + tn) = kl x+tm−(y+tn)=kl

整理后得到
( m − n ) t − k l = y − x (m - n)t - kl = y - x (m−n)t−kl=y−x

这个式子其实就是一个不定方程 a x + b y = c ax + by = c ax+by=c,我们要求的 t t t 就是这里的 x x x。

上面的式子还可以看作一个同余方程,如下
( m − n ) t ≡ y − x ( m o d l ) (m - n)t\equiv y - x\pmod l (m−n)t≡y−x(modl)

如果 m − n < 0 m - n < 0 m−n<0 那么等价于求解
( n − m ) t ≡ x − y ( m o d l ) (n - m)t\equiv x - y\pmod l (n−m)t≡x−y(modl)


2. 代码实现

cpp 复制代码
#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 = c
    LL 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;
}
相关推荐
Geo_V3 小时前
提示词工程
人工智能·python·算法·ai
侯小啾3 小时前
【22】C语言 - 二维数组详解
c语言·数据结构·算法
TL滕3 小时前
从0开始学算法——第一天(如何高效学习算法)
数据结构·笔记·学习·算法
傻童:CPU3 小时前
DFS迷宫问题
算法·深度优先
B站_计算机毕业设计之家3 小时前
计算机视觉:python车辆行人检测与跟踪系统 YOLO模型 SORT算法 PyQt5界面 目标检测+目标跟踪 深度学习 计算机✅
人工智能·python·深度学习·算法·yolo·目标检测·机器学习
一个不知名程序员www4 小时前
算法学习入门---前缀和(C++)
c++·算法
jackzhuoa4 小时前
Rust API 设计的零成本抽象原则:从语言基石到工程实践
算法·rust
我不是彭于晏丶4 小时前
238. 除自身以外数组的乘积
数据结构·算法
兮山与4 小时前
算法25.0
算法