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

题目传送门

这是一道模板题(bushi)

首先,说实话,我感觉做这道题比较吃力,毕竟我是刚刚背过\(exgcd\)代码,并且只是小小的对找正整数解有所了解,所以写了半天才做出来。做这种数论题确实是对计算能力和逻辑思维的考验,而且从难度上看,我这个蒟蒻能做出绿题,也有点小小的成就感。

今天开始停课备战\(CSP\),下午在机房打板子,打到\(exgcd\),突然发现我板子题还没\(A\),其他的拓展题倒是做了不少,遂开始切板子题

思路

其实思路非常简单,就是代码实现细节真的比较多(而且我貌似写的很麻烦,写了足足有130多行)。首先用真板子来求出一组特解,然后判断是否有解。在找最小整数解的时候,要特判一波:如果有一组解能满足\(x\)\(y\)都是正整数,那么就要输出5个数。反之,则只需要输出\(x\)\(y\)的最小整数解。

那么首先最好写的就是求出的特解都是正整数的情况,只需要用一波取模的常规操作求出一个最小正整数解,那么同时另一个解就是最大正整数解。即\(x\)最小时,\(y\)最大。就这样求出四个最值。个数可以通过最大解和最小解中间差了几个\(a/d\)\(b/d\)即可(原理就是求最小正整数解的操作,详情可见【青蛙的约会】)。然后中间稍微手玩一下,就能找到个数与最值之间的关系。

                        ll x1=(x1%(b/d)+(b/d))%(b/d);
			ll y1=(y1%(a/d)+(a/d))%(a/d);//求出最小值
			if(x1==0){//特判,必须是正整数
				x1=b/d;
			}
			if(y1==0){
				y1=a/d;
			}
			ll x2=(c-y1*b)/a;
			ll y2=(c-x1*a)/b;//根据最小值推出最大值
			ll sum;
			if(x2%(b/d)==0){//手玩即可找到规律,推导为小学数学水平
				sum=x2/(b/d);
				x1=a/d;
			}
			else{
				sum=x2/(b/d)+1;
				x1=x2%(b/d);
			}

那么接下来就是\(x\)\(y\)这一组特解中有一个为非正整数的情况了,虽然这种情况相对复杂,但是原理是一样的。假设\(x\)是负的,那么\(y\)必然是正的,否则不可能得到正数结果。那么我们就让\(x\)不断地加上\(b/d\),为保持等式成立,也要让\(y\)同时减去\(a/d\)。这样将\(x\)变为正数。如果\(x\)变为正数后\(y\)变为了负数,那么就说明不可能有正整数解,这时候只要进行一步常规取模操作,输出最小正整数解即可。但是如果\(x\)变为正数后\(y\)依然是正数,就相当于变回了第一种情况,再操作即可。

思路就是这么简单,但是代码实现的确有难度。首先我们思考\(x\)需要几个\(b/d\)才能变成正数,首先可以将\(x\)变为相反数,便于我们进行除法。由于除法是向下取整,所以我们用\(x\)除以\(b/d\)还要再加一个\(b/d\)才行(即使整除也要加,因为0并不是正整数)。所以我们就可以用\(x/(b/d)+1\)来求得加的\(b/d\)的个数,同时\(y\)也要减。然后再按照上述思路判断即可。

                                if(x<=0){
				x=-x;
				int k=x/(b/d)+1;
				x-=k*(b/d);
				x=-x;
				y-=k*(a/d);
				if(y>0){//有正整数解 
					ll x1=x,y2=y,sum,y1;
					if(y2%(a/d)==0){
						sum=y2/(a/d);
						y1=a/d;
					}
					else{
						sum=y2/(a/d)+1;
						y1=y2%(a/d);
					}
					ll x2=(c-y1*b)/a;
					printf("%lld %lld %lld %lld %lld\n",sum,x1,y1,x2,y2);
					continue;
				}
				else{//无正整数解 
					ll x1=(xx%(b/d)+(b/d))%(b/d);
					ll y1=(yy%(a/d)+(a/d))%(a/d);
					if(x1==0){
						x1=b/d;
					}
					if(y1==0){
						y1=a/d;
					}
					printf("%lld %lld\n",x1,y1);
					continue;
				}
			}

做完了

代码

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
#include<cstdlib>
#include<ctime>
#include<cstring>
#include<queue>
using namespace std;
typedef long long ll;
int T;
inline ll read(){
	ll x=0,f=1;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-') f=-1;ch=getchar();}
	while(isdigit(ch)){x=x*10+ch-48;ch=getchar();}
	return x*f;
}
ll exgcd(ll a,ll b,ll &x,ll &y){
	if(b==0){
		x=1;y=0;
		return a;
	}
	ll d=exgcd(b,a%b,x,y);
	ll z=x;x=y;y=z-y*(a/b);
	return d;
} 
int main()
{
	scanf("%d",&T);
	while(T--){
		ll a,b,c,x,y;
		a=read();b=read();c=read();
		ll d=exgcd(a,b,x,y);
		if(c%d!=0){
			printf("-1\n");
			continue;
		}
		else{
			x*=c/d,y*=c/d;
			ll xx=x,yy=y;
			if(x<=0){
				x=-x;
				int k=x/(b/d)+1;
				x-=k*(b/d);
				x=-x;
				y-=k*(a/d);
				if(y>0){//有正整数解 
					ll x1=x,y2=y,sum,y1;
					if(y2%(a/d)==0){
						sum=y2/(a/d);
						y1=a/d;
					}
					else{
						sum=y2/(a/d)+1;
						y1=y2%(a/d);
					}
					ll x2=(c-y1*b)/a;
					printf("%lld %lld %lld %lld %lld\n",sum,x1,y1,x2,y2);
					continue;
				}
				else{//无正整数解 
					ll x1=(xx%(b/d)+(b/d))%(b/d);
					ll y1=(yy%(a/d)+(a/d))%(a/d);
					if(x1==0){
						x1=b/d;
					}
					if(y1==0){
						y1=a/d;
					}
					printf("%lld %lld\n",x1,y1);
					continue;
				}
			}
			if(y<=0){
				y=-y;
				int k=y/(a/d)+1;
				y-=k*(a/d);
				y=-y;
				x-=k*(b/d);
				if(x>0){//有正整数解 (y为最小整数解 
					ll y1=y,x2=x,x1,sum;
					if(x2%(b/d)==0){
						sum=x2/(b/d);
						x1=b/d;
					}
					else{
						sum=x2/(b/d)+1;
						x1=x2%(b/d);
					}
					ll y2=(c-x1*a)/b;
					printf("%lld %lld %lld %lld %lld\n",sum,x1,y1,x2,y2);
					continue;
				}
				else{
					ll x1=(xx%(b/d)+(b/d))%(b/d);
					ll y1=(yy%(a/d)+(a/d))%(a/d);
					if(x1==0){
						x1=b/d;
					}
					if(y1==0){
						y1=a/d;
					}
					printf("%lld %lld\n",x1,y1);
					continue;
				}
			}
			ll x1=(x1%(b/d)+(b/d))%(b/d);
			ll y1=(y1%(a/d)+(a/d))%(a/d);
			if(x1==0){
				x1=b/d;
			}
			if(y1==0){
				y1=a/d;
			}
			ll x2=(c-y1*b)/a;
			ll y2=(c-x1*a)/b;
			ll sum;
			if(x2%(b/d)==0){
				sum=x2/(b/d);
				x1=a/d;
			}
			else{
				sum=x2/(b/d)+1;
				x1=x2%(b/d);
			}
		}
	}
	return 0;
}

中间出了两个小锅,耗费了我大量时间:一个是我忘记判x>0&&y>0的情况了,可能是因为这种情况相比于其它的情况太水了,所以忘记了。第二个,由于y<0和x<0的代码绝大部分都是一样的,只不过需要该两个变量名,所以我直接复制粘贴之后一个个改变量名。结果漏改了一个,导致出锅。

警示:这告诉我,在做这种思维量比较大的题时,一定要将思路写下来,不然很容易漏情况或者是写挂。而且在复制粘贴相似部分代码的时候,一定一定要记得改完变量名,最好是不要复制,重新打一遍,这样基本能保证不会出错。

掌握知识点的确是要靠做题的,记得我初学搜索与回溯,也就是现在用来打暴力的神器,死活就是学不会,我就是不明白它是怎么递归的,我就是不明白为什么要回溯。但是当我抄上几篇dfs题解,找到感觉后,我独立切掉了一道同样让我耗费了大量精力的【数独】。虽然过程很难,但当切掉一道标志性题目的时候,我仿佛对这个知识点的掌握就上升了一个档次。相似的知识点和题目还有很多:比如线段树——【方差】;区间dp——【能量项链】;倍增——【严格次小生成树】;矩阵快速幂——【斐波那契数列】......这些我切过后就掌握住知识点的题,都深深印在我的脑海里。确确实实验证了眼高手低这一亘古不变的道理。而今天,切掉这道板子题,写这篇博客,是为了记录我学会了\(exgcd\),也是对学习之旅中的经验总结。

posted @ 2020-11-04 19:18  徐明拯  阅读(218)  评论(0编辑  收藏  举报