题解:P5656 【模板】二元一次不定方程 (exgcd)
这是有必要的。因为我 CSP-S 考前两个周还不会 exgcd。
upd 10.27 修锅。
::::warning[前置知识]
-
- 特别地,我们规定
\gcd(x,0)=x 。 ::::
exgcd,全称扩展欧几里得算法(Extended Euclidean algorithm, EXGCD),常用于求
算法过程
它与 gcd 是类似的。
回顾一下普通 gcd 的过程:
:::::info[如果你不知道欧几里得算法(gcd)]
欧几里得算法(gcd)可以在
它的过程是这样的:
- 假设当前要求
\gcd(a,b) 。 - 如果
b=0 ,那么\gcd(a,0)=a (人为规定的),于是返回。 - 否则,递归求解
\gcd(b,a\bmod b) 的值,并返回。
在这一过程中,我们用到了一个定理
那么它的复杂度为何是
::::info[gcd 为何是
我们考虑当前的
那么接下来它会有以下两种情况:
每次走上面的情况时,下一次都一定会走下面的情况,而
如果
如果
:::::
那么我们如果要求
是可以的。假设下面方程的一组解是
::::success[证明]
考虑设
证毕。 ::::
但是我们不能无限制的递归下去。我们需要找寻一个边界条件。它就是
即可。
证明:显而易见的。直接把它代入原方程。
::::success[一个简单的 exgcd 代码实现]
#define ll long long
ll x,y;
void exgcd(ll a,ll b){
if(b==0){
x=1;
y=0;
return;
}
exgcd(b,a%b);
ll t=x;
x=y;
y=t-a/b*y;
}
你还可能见到另外一种写法。
void exgcd(ll &x,ll &y,ll a,ll b){
if(b==0){
x=1;
y=0;
return;
}
exgcd(x,y,b,a%b);
ll t=x;
x=y;
y=t-a/b*y;
}
这里面的 & 是取地址符,具体来说,当你递归调用时,如果内层的
例题 1
例题
求关于
对于
::::success[Solution]
正常我们解决 exgcd 问题都需要
没关系!我们只需要把方程转化为
由于
但是 exgcd 解出来的只是一组解,所以我们要把解出来的
因为
#include<iostream>
#include<cstdio>
#define ll long long
using namespace std;
ll x,y;
void exgcd(ll a,ll b){
if(b==0){
x=1;
y=0;
return;
}
exgcd(b,a%b);
ll t=x;
x=y;
y=t-a/b*y;
}
int main(){
ll a,b;
cin>>a>>b;
exgcd(a,b);
x=(x%b+b)%b;
cout<<x;
return 0;
}
::: ::::
变式 1
变式
求
::::success[Solution]
如果我们求出
:::error[为什么
把上面的代码改一改就能过。 ::::
事实上,这个变式的意义是可以通过 exgcd 来求任意一个整数的逆元,相比起费马小定理与快速幂的组合,适用范围更广。
例题 2
例题
exgcd 基础,【模板】exgcd 就不基础。
对于普通的 exgcd,它有非常多的不同。
裴蜀定理的应用
正常的 exgcd 是解
别怕,我们假设
那么
不难发现
所以我们需要保证
如果不等于零,可以直接判断没有整数解。
:::info[为何“如果不等于零,可以直接判断没有整数解”?] 这就是裴蜀定理。P4549 题解区有关于这个的证明。 :::
但是现在我们要求它的正整数解的数量,所有正整数解中
我们发现这个东西显然是不可能用普通的 exgcd 做了。假设我们现在已经通过 exgcd + 裴蜀定理解决了
基础:单推多
这个是以下推导的基础。
首先,我们设一组普通解为
那么显然有:
我们设
那么有:
于是得到
由于
这是一个非常神秘的式子。我们需要找一组通解。
直接给结论:
其中
:::info[怎么找到的通解?]
考虑人脑是如何解这个方程的,以
不难考虑让
也就是说,令
就可以得到一组特解。
但是
所以我们考虑令
:::
那么我们得到
x 的最小值
根据
我们需要求出最大的
考虑推导。
最后一步取上等是因为我们不能让
于是可以
:::info[二分方案(不推荐)]
用二分求解
显然
缺点是该做法的复杂度是
y 的最大值
考虑
反解得到
在
那么只要
正整数解的判断
只要
y 的最小值
先给结论:
证明:
考虑下面的方程:
由于
设
根据上面的推导,可以得到
其中
由于
说明
由于
但是这样并不严谨:因为
正整数解的个数
给结论:设答案为
我们考虑用小学的植树问题来证明这个结论。
现在有一排树,间隔相等为
d ,第一棵树坐标为x_1 ,最后一棵树坐标为x_n ,那么一共有多少棵树?(保证x_n-x_1 是d 的倍数)
这个是很简单的:答案是
考虑
现在我们考虑把这个问题代入到当前的情况。
把
根据上面的推导,树的间距是
所以你就能解了。
:::info[为何每
考虑
那么
那么存在一组解
由于是在
于是证毕。 :::
x 的最大值
这是很简单的。
由于
::::error[我们没有处理无正整数解的情况!]
其实不难发现,经过上面的讨论,我们已经把没有正整数解的情况的
你还记得
同理我们可以发现
::::success[P5656 代码]
#include<bits/stdc++.h>
#define ll long long
using namespace std;
ll x,y;
void exgcd(ll a,ll b){
if(b==0){
x=1;y=0;return;
}
exgcd(b,a%b);
ll t=x;
x=y;y=t-a/b*y;
}
ll a,b,c;
void solve(){
scanf("%lld%lld%lld",&a,&b,&c);
ll g=__gcd(a,b);
if(c%g!=0){
printf("-1\n");return;
}
exgcd(a,b);
ll x0=c/g*x,y0=c/g*y;
ll p=ceil(1.0*(1-x0)/(b/g));
ll xmin=x0+p*(b/g);
ll ymax=y0-p*(a/g);
if(ymax<=0){
ll ymin=(ymax%(a/g)+(a/g))%(a/g);
if(ymin==0)ymin=a/g;
printf("%lld %lld\n",xmin,ymin);
}
else{
ll ymin=(ymax)%(a/g);
if(ymin==0)ymin=a/g;
ll cnt=(ymax-ymin)/(a/g)+1;
ll xmax=(c-b*ymin)/a;
printf("%lld %lld %lld %lld %lld\n",cnt,xmin,ymin,xmax,ymax);
}
}
int main(){
int T;
scanf("%d",&T);
while(T--)solve();
}
::::
例题 3
P9796 [NERC 2018] Fractions
给你一个整数
::::info[Solution] 先给结论。
结论:如果有解,一定存在一组
证明:考虑通分。
根据裴蜀定理,上面方程有解的条件是
设
由于
考虑令
但是如果
如果你构造多个,条件更强,更无法满足。
找出
可以用 exgcd 求出其的一组解,然后用 P5656 的代码求出其最小的正整数
:::success[AC Code]
#include<bits/stdc++.h>
#define ll long long
using namespace std;
ll n,x,y;
void exgcd(ll a,ll b){
if(b==0){
x=1;y=0;return;
}
exgcd(b,a%b);
ll tmp=x;
x=y;
y=tmp-a/b*y;
}
//const int N=;
int main(){
scanf("%lld",&n);
ll a=-1,b=0;
ll t=n;
for(int i=2;i*i<=t;i++){
if(n%i!=0)continue;
ll tmp=1;
while(t%i==0){
t/=i;tmp*=i;
}
if(t==1){
printf("NO\n");
return 0;
}
a=tmp;b=n/tmp;
break;
}
if(a==-1){
printf("NO\n");
return 0;
}
exgcd(a,b);
printf("YES\n2\n");
ll g=__gcd(a,b);
ll x0=(n-1)*x,y0=(n-1)*y;
ll p=ceil(1.0*(1-x0)/(b/g));
ll xmin=x0+p*(b/g);
ll ymax=y0-p*(a/g);
printf("%lld %lld\n%lld %lld",xmin,b,ymax,a);
return 0;
}
::: ::::