exgcd

定义

又名扩展欧几里得算法(辗转相除法)

是用来求 \(ax+by=gcd(a,b)\) 中未知数的算法


算法证明

拿到一组 \(a,b\) ,设 \(G=gcd(a,b)\)

目标:求出满足 \(ax+by=G(1)\)\(x\)\(y\)

如果 已知一组 \(x_2,y_2\) ,满足 \(bx_2+\) \((a\bmod b)y_2=G(2)\)

此时结合 \((1)(2)\)
\(ax+by=\) \(bx_2+\) \((a\bmod b)y_2(3)\)

那么当 如果 满足时,目标就成了求满足 \((3)\)\(x,y\),其中 \(a,b,x_2,y_2\) 均已知

根据取模运算是 \(a\bmod b=a-b\times \lfloor{\dfrac{a}{b}}\rfloor\)
所以方程 \((3)\) 实则是

\(ax+by=bx_2+(a-b\times \lfloor{\dfrac{a}{b}}\rfloor)y_2\)

\(->\) \(ax+by=bx_2+ay_2-b\times \lfloor{\dfrac{a}{b}}\rfloor y_2\)

\(->\) \(ax+by=ay_2+b(x_2-\lfloor{\dfrac{a}{b}}\rfloor y_2)\)

那么在根据方程 \(ax+by=G(1)\) 得出一组必然的解:

\(x=y_2,y=x_2-\lfloor{\dfrac{a}{b}}\rfloor y_2(4)\)

可见只需求出 \(x_2,y_2\) ,就能求出正确的 \(x,y\),问题就转化成了求 \(x_2,y_2\)

将方程 \((1)\)\((2)\) 对比一下:

\(ax+by=G(1)\)

\(bx_2+\) \((a\bmod b)y_2=G(2)\)

发现原方程的 \(a\) 变成了 \(b\),原方程的 \(b\) 变成了 \(a\) 而已

所以新的方程也可以看做 \(ax+by=G\) 的形式,然后按照上面的程序进行下来,发现问题又变为求 \(x_3,y_3\) 再求 \(x_4,y_4\) \(......\) 即一个递归的过程

递归过程中 \(a,b\) 不断被替换为 \(b,a\bmod b\),这个过程和普通的欧几里得是一样的,所以最后会出现 \(a_n=G,b_n=0\)

那么这特就是最后一层,此时就要直接返回了,需要一组 \(x_n,y_n\) 满足 \(a_nx_n+b_ny_n=G(5)\),然而 \(a_n=G,b_n=0\),所以只要 \((5)\)\(x_n\)\(1\) 就必定满足了,\(y_n\) 就随便取个 \(0\) 就行了

最后一层结束后,就开始返回,知道最上一层,每一层都可以通过下一层的 \(x_{k+1},y_{k+1}\) 求出当前层的 \(x_k,y_k\)

整个过程就是:以辗转相除的方式向下递进,不断缩小系数,保证会出现有确定解的最后一层


exgcd板子

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,y,x);
    y-=a/b*x;
    return d;
}

\(注意:x,y不要忘了取地址!\)


答案处理

对于更为一般的方程,\(ax+by=c\),他有解当切仅当 \(c\bmod d=0\)(设 \(d=gcd(x,y)\))。通过 \(exgcd\) 求出一组特解 \(x_0,y_0\),令他们同时 \(\times c/d\),就得到了 \(ax+by=c\) 的一组特解 \(\lfloor{\dfrac{c}{d}}\rfloor x_0,\lfloor{\dfrac{c}{d}}\rfloor y_0\)

事实上,方程 \(ax+by=c\) 的通解可以表示为:

\(x=\lfloor{\dfrac{c}{d}}\rfloor x_0+k\lfloor{\dfrac{b}{d}}\rfloor\) \(,\) \(y=\lfloor{\dfrac{c}{d}}\rfloor y_0-k\lfloor{\dfrac{a}{d}}\rfloor\)

其中 \(k\) 取遍正数集合,其余量上面已设


例题1:同余方程

题目链接 同余方程

问题处理

题目问的是满足 \(ax\bmod b=1\) 的最小正整数(a,b均为正整数)

问题可以转化成求 \(ax+by=1\)\(x\) 的值,其中 \(y\) 是个引入的辅助数(y一定是一个负数,但是写成 \(+\) 的形式以便 \(exgcd\) 算法)

问题仍需转化,\(exgcd\) 求得是 \(ax+by=gcd(a,b)\),所以求方程 \(ax+by=m\) 必须满足的条件就是 \(m\bmod gcd(a,b)=0\)


简单证明一下:

由最大公因数的定义,\(a,b\) 均是 \(gcd(a,b)\) 的倍数,因为 \(m=ax+by\),所以 \(m\) 一定是 \(gcd(a,b)\) 的倍数,即 \(m\bmod gcd(a,b)=0\)


可得这道题中,必须满足 \(1\bmod gcd(a,b)=0\),那么 \(gcd(a,b)\) 就必须等于 \(1\) 了,即 \(a,b\) 互质(数据一定是满足的)

之后通过上述的 \(exgcd\) 算法即可,但是题目要求 \(x\) 的最小值,仅仅 \(exgcd\) 无法满足,所以需要答案处理


答案处理

求出来的 \(x,y\) 仅仅满足 \(ax+by=1\),而 \(x\) 不一定是最小正整数解。有可能太大,也可能是负数

解决方案是:\(x\) 批量地减去或加上 \(b\),能保证 \(ax+by=1\),因为:

\(ax+by=1\)

\(-> ax+by+k\times ba-k\times ba=1\)

\(-> a(x+kb)+(y-ka)b=1\)

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

2.加上或减去 \(b\) 不会使 \(x\) 错过任何解,可以这么理解:


已经求出一组 \(x,y\) 使得 \(ax+by=1\),也就是 \((1-ax)\div b=y\)\(y\) 是整数,可见目前 \(1-ax\)\(b\) 的倍数

现在给 \(x\) 加上 \(kb\)(k为整数),则变为:

\((1-a(x+kb))\div b\)

\(=(1-ax)\div b+ak\)

仍满足其为 \(b\) 的倍数,由此当 \(x\) 的变化量为 \(b\) 的倍数时,等式仍满足


因此到最后,如果 \(x\) 太小就不断 \(+b\) 直到 \(>=0\),反之则一直减直到最小正整数值,就是这么写

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

总代码如下

#include<bits/stdc++.h>
#define endl '\n'
using namespace std;
template<typename Tp> inline void read(Tp&x)
{
    x=0;register bool z=1;
    register char c=getchar();
    for(;c<'0'||c>'9';c=getchar()) if(c=='-') z=0;
    for(;'0'<=c&&c<='9';c=getchar()) x=(x<<1)+(x<<3)+(c^48);
    x=(z?x:~x+1);
}
int a,b,x,y;
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,y,x);
    y-=a/b*x;
    return d;
}
signed main()
{
    #ifndef ONLINE_JUDGE
    freopen("in.txt","r",stdin);
    freopen("out.txt","w",stdout);
    #endif
    read(a),read(b);
    exgcd(a,b,x,y);
    x=(x%b+b)%b;
    cout<<x;
}

例题2:青蛙的约会

题目链接 青蛙的约会

问题处理

两点在数轴上,\(A,B\)\(A\)\(x\),这个位置,\(B\)\(y\) 这个位置,数轴长度为 \(l\)\(A\) 每次向右移 \(m\) 个长度,\(B\) 每次向右移 \(n\) 个长度,题意表明他是一个环,也就是说 \(A_k\) 下一次 \(A_{k+1}\) 到达 \((A_k+m)\bmod l\) 的位置,\(B_k\) 下一次 \(B_{k+1}\) 到达 \((B_k+n)\bmod l\) 的位置

设他跳 \(k\) 次后相遇,即 \(A_k=B_k\)

带进去就是 \((x+km)\bmod l=(y+kn)\bmod l\)


\(引入一条性质:\)

左右括号内同时减(加)去一个数等式仍然成立:

如果 \(a\bmod P=b\bmod P\)

\((a-c)\bmod P=(b-c)\bmod P\)


那么原式就可以变为

\(->((x-y)+km)\bmod l=kn\bmod l\) (左右两边括号内同时减去 \(y\) )

\(->((x-y)+k(m-n))\bmod l=0\) (左右两边同时减去 \(kn\) )

那么根据该方程,\((x-y)+k(m-n)\) 一定是 \(l\) 的倍数,不妨设 \((x-y)+k(m-n)=-t\times l\) ( \(-t\) 为新设的倍数值)

\(->k(m-n)+tl=y-x\) (现在知道为什么设 \(-t\) 了吧)

于是终于将推导式转化成了 \(exgcd\) 的形式,\(k(m-n)+tl=gcd(m-n,l)\) ,借鉴上一道题的证明,\(y-x\) 一定是 \(gcd(m-n,l)\) 的倍数

就需要思考一下数据是否满足,题目要求如果两点永远无法相遇则输出 \(Impossible\),所以如果不满足 \(y-x\)\(gcd(m-n,l)\) 的倍数时,输出 \(Impossible\) 即可

\(exgcd\) 求出 \(k(m-n)+tl=gcd(m-n,l)\)\(k\) 之后,对他进行答案处理即可


答案处理

1.借鉴上题的答案处理,我们刚刚求出来的时第一层的结果,但他显然不一定是第一次相遇的结果,例题1答案处理的式子是 \(x=(x\bmod b+b)\bmod b\)

2.我们导出来的是 \(k(m-n)+tl=y-x\),而他求的是 \(k(m-n)+tl=gcd(m-n,l)\),满足 \(y-x\)\(gcd(m-n,l)\) 的倍数,所以真正的答案应该是求出的答案 \(\times (y-x)\div gcd(m-n,l)\),设 \(c=y-x,d=gcd(m-n,l)\),则答案应 \(\times c/d\)

借鉴这两点,推导出这道题的答案处理式子为

x=((x*(c/d))%(b/d)+(b/d))%(b/d);

\(x\) 表示求出来那个 \(k\)\(b\) 表示 \(l\)\(c,d\) 上面已设,之前的 \(b\)\(\div d\)


总代码如下

#include<bits/stdc++.h>
#define int long long 
#define endl '\n'
using namespace std;
template<typename Tp> inline void read(Tp&x)
{
    x=0;register bool z=1;
    register char c=getchar();
    for(;c<'0'||c>'9';c=getchar()) if(c=='-') z=0;
    for(;'0'<=c&&c<='9';c=getchar()) x=(x<<1)+(x<<3)+(c^48);
    x=(z?x:~x+1);
}
int x,y,n,m,l,xx,yy;
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,y,x);
    y-=a/b*x;
    return d;
}
signed main()
{
	#ifndef ONLINE_JUDGE
    freopen("in.txt","r",stdin);
    freopen("out.txt","w",stdout);
    #endif
    read(xx),read(yy),read(m),read(n),read(l);
    int a=m-n,b=l,c=yy-xx;
    if(a<0) a=-a,c=-c;
    int d=exgcd(a,b,x,y);
    if((c%d)!=0) cout<<"Impossible";
    else cout<<(((x*(c/d))%(b/d)+(b/d))%(b/d));
}

注意事项

放到 \(exgcd\) 里的这个 \(a\) 需要保证是正的,同时 \(m-n\) 对应的一定是 \(y-x\),反之,如果 \((m-n)<0\),将他改为 \(n-m\) 的同时,\(y-x\) 也要改为 \(x-y\),即代码中的这一行

int a=m-n,b=l,c=yy-xx;
if(a<0) a=-a,c=-c;

例题3:倒酒

题目链接 倒酒

问题处理

问题比较简单,也不需要怎么证明,输入 \(a,b\)

显然,能够倒出的最小数为 \(d=gcd(a,b)\)

而倒 \(A\) 的次数 \(x\) 和倒 \(B\) 的次数 \(y\) 满足 \(by-ax=d\)

放到 \(exgcd\) 中即可


答案处理

难点在于答案处理,怎么搞出来最小解?

显然,求出的 \(x\) 为负的,所以输出的应为 \(-x\)

那我们按照前两题的思路这么打

x=(x%m-m)%m;
y=(y%n+n)%n;

但是这样是错误的,\(Wa\) \(90分\)

根据我们上面打过的总的答案处理,$x=\lfloor{\dfrac{c}{d}}\rfloor x_0+k\lfloor{\dfrac{b}{d}}\rfloor $ \(,\) \(y=\lfloor{\dfrac{c}{d}}\rfloor y_0-k\lfloor{\dfrac{a}{d}}\rfloor\),本道题的 \(c=d\),所以 \(x=x_0+k\lfloor{\dfrac{b}{d}}\rfloor\) \(,\) \(y=y_0+k\lfloor{\dfrac{a}{d}}\rfloor\)

同时保证最后输出的是正的最小值,先让 \(x\)\(a\) 变成 \(-x,-a\),因为我们求出来的这个 \(x\) 显然是负数

之后,在保证 \(x,y\)\(>0\) 的前提下不断地枚举这个 \(k\),根据上面这个公式:

\(x<0\) 时,让他不断加上 \(\lfloor{\dfrac{b}{d}}\rfloor\) ,当他大于 \(0\) 的一瞬间,就是正的最小值

同理,当 \(x>=0\) 时,\(y\) 做出对应调整,不断地减去 \(\lfloor{\dfrac{a}{d}}\rfloor\)

如下

x*=-1;a*=-1;
while((x<0)||(y<0))
    x+=(x<0)?b/d:0,
    y-=(x>=0)?a/d:0;

需着重理解一下


总代码如下

#include<bits/stdc++.h>
#define int long long 
#define endl '\n'
using namespace std;
template<typename Tp> inline void read(Tp&x)
{
    x=0;register bool z=1;
    register char c=getchar();
    for(;c<'0'||c>'9';c=getchar()) if(c=='-') z=0;
    for(;'0'<=c&&c<='9';c=getchar()) x=(x<<1)+(x<<3)+(c^48);
    x=(z?x:~x+1);
}
int x,y,n,m;
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,y,x);
    y-=a/b*x;
    return d;
}
signed main()
{
	#ifndef ONLINE_JUDGE
    freopen("in.txt","r",stdin);
    freopen("out.txt","w",stdout);
    #endif
    read(n),read(m);
    int d=exgcd(n,m,x,y);
    x*=-1;n*=-1;
    while((x<0)||(y<0))
        x+=(x<0)?m/d:0,
        y-=(x>=0)?n/d:0;
    cout<<d<<endl<<x<<' '<<y;
}

例题4:一元二次不定方程(模板)

题目链接一元二次不定方程

问题处理

给定一式子 \(ax+by=c\)

  1. \(x_{min},y_{min}\)
  2. \(x_{max},y_{max}\)
  3. 求解的个数

\(x_{min},y_{min}\)

通过 \(exgcd\) 很容易求出 \(x,y\),在通过简单的答案处理即可求出 \(x_{min},y_{min}\)


\(x_{max},y_{max}\)

不难理解,当 \(x\) 为最大值时,即对应 \(y\) 的最小值,同理通过此方式可求出 \(y\) 的最大值


求解的个数

现拿到一组解 \(ax_{min}+y_{max}=c\)

使 \(x\) 不断加上 \(b\),使 \(y\) 不断减去 \(a\),直到 \(ax_{max}+by_{min}=c\)

这样就求出了不定方程的所有解,不难发现,此过程中,共出现了 \(\lfloor\dfrac{x_{max}-x_{min}}{b}\rfloor +1\) 或者说 \(\lfloor\dfrac{y_{max}-y_{min}}{a}\rfloor +1\) 组解,即解的个数


答案处理

首先设 \(x=x_{min},y=y_{min},d=\gcd(a,b)\)


处理最小值

借鉴例题2和总答案处理中的通解,\(x_{min},y_{min}\) 的答案处理为

x=(x*(c/d)%(b/d)+(b/d))%(b/d);
y=(y*(c/d)%(a/d)+(a/d))%(a/d);

但同时,题目要求 \(x,y\) 均为正整数,所以 \(0\) 也不行,故还需要

x=(x?x:x+(b/d)),y=(y?y:y+(a/d));

处理最大值

根据我们问题处理中最大值的求法,得出最大值为

x_max=(c-y*b)/a,y_max=(c-x*a)/b;

处理解的个数

直接根据我们上面推出的式子

ans=(x_max-x)/(b/d)+1;

或者

ans=(y_max-y)/(a/d)+1;

最后注意

题目要求若不存在 \(x,y\) 均为正整数的解,只输出 \(x_{min},y_{min}\),只需要判断 \(x_{max},y_{max}\) 中是否存在 \(\leq 0\) 的即可


总代码如下

#include<bits/stdc++.h>
#define int long long 
#define endl '\n'
using namespace std;
template<typename Tp> inline void read(Tp&x)
{
    x=0;register bool z=1;
    register char c=getchar();
    for(;c<'0'||c>'9';c=getchar()) if(c=='-') z=0;
    for(;'0'<=c&&c<='9';c=getchar()) x=(x<<1)+(x<<3)+(c^48);
    x=(z?x:~x+1);
}
int T,a,b,c,x,y;
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,y,x);
    y-=a/b*x;
    return d;
}
signed main()
{
    #ifndef ONLINE_JUDGE
    freopen("in.txt","r",stdin);
    freopen("out.txt","w",stdout);
    #endif
    read(T);
    while(T--)
    {
        read(a),read(b),read(c);
        x=y=0;
        int d=exgcd(a,b,x,y);
        if(c%d) 
        {
            cout<<-1<<endl;
            continue;
        }
        x=((x*(c/d))%(b/d)+(b/d))%(b/d);
        y=((y*(c/d))%(a/d)+(a/d))%(a/d);
		x=(x?x:x+(b/d)),y=(y?y:y+(a/d));
        int x_max=(c-y*b)/a,y_max=(c-x*a)/b,ans=(y_max-y)/(a/d)+1;
        if(x_max<=0||y_max<=0) cout<<x<<' '<<y<<endl;
        else cout<<ans<<' '<<x<<' '<<y<<' '<<(c-y*b)/a<<' '<<(c-x*a)/b<<endl;
    }
}
posted @ 2023-12-30 19:14  卡布叻_周深  阅读(44)  评论(2编辑  收藏  举报