1. 程式人生 > 實用技巧 >洛谷P5656 【模板】二元一次不定方程(exgcd)

洛谷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\),也是對學習之旅中的經驗總結。