ACM大數之間求最大公約數
寫一個程式,求兩個正整數的最大公約數。如果兩個正整數都很大,有什麼簡單的演算法嗎?
分析與解法
求最大公約數是一個很基本的問題。早在公元前300年左右,歐幾里得就在他的著作《幾何原本》中給出了高效的解法——輾轉相除法。輾轉相除法使用到的原理很聰明也很簡單,假設用f(x, y)表示x,y的最大公約數,取k = x/y,b = x%y,則x = ky + b,如果一個數能夠同時整除x和y,則必能同時整除b和y;而能夠同時整除b和y的數也必能同時整除x和y,即x和y的公約數與b和y的公約數是相同的,其最大公約數也是相同的,則有f(x, y)= f(y, y % x)(y > 0),如此便可把原問題轉化為求兩個更小數的最大公約數,直到其中一個數為0,剩下的另外一個數就是兩者最大的公約數。輾轉相除法更詳細的證明可以在很多的初等數論相關書籍中找到,或者讀者也可以試著證明一下。
示例如下:
f(42, 30)=f(30, 12)= f(12, 6)=f(6, 0)= 6
【解法一】
最簡單的實現,就是直接用程式碼來實現輾轉相除法。從上面的描述中,我們知道,利用遞迴就能夠很輕鬆地把這個問題完成。
具體程式碼如下:
int gcd(int x, int y)
{
return (!y)?x:gcd(y, x%y);
}
【解法二】
在解法一中,我們用到了取模運算。但對於大整數而言,取模運算(其中用到除法)是非常昂貴的開銷,將成為整個演算法的瓶頸。有沒有辦法能夠不用取模運算呢?
採用類似前面輾轉相除法的分析,如果一個數能夠同時整除x和y,則必能同時整除x-y和y;而能夠同時整x-y和y的數也必能同時整除x和y,即x和y的公約數與x-y和y的公約數是相同的,其最大公約數也是相同的,即f(x, y)= f(x-y, y),那麼就可以不再需要進行大整數的取模運算,而轉換成簡單得多的大整數的減法。
在實際操作中,如果x<y,可以先交換(x, y)(因為(x, y)=(y, x)),從而避免求一個正數和一個負數的最大公約數情況的出現。一直迭代下去,直到其中一個數為0。
示例如下:
f(42, 30)=f(30, 12)=f(12, 18)= f(18, 12)= f(12, 6)= f(6, 6)= f(6, 0)= 6
解法二的具體程式碼如下:
程式碼清單2-15
BigInt gcd(BigInt x, BigInt y)
{
if(x < y)
return gcd(y, x);
if(y == 0)
return x;
else
return gcd(x - y, y);
}
程式碼中BigInt是讀者自己實現的一個大整數類(所謂大整數當然可以是成百上千位),那麼就要求讀者過載該大整數類中的減法運算子“-”,關於大整數的具體實現這裡不再贅述,若讀者只是想驗證該演算法的正確性,完全可使用系統內建的int型來測試。
這個演算法,免去了大整數除法的繁瑣,但是同樣也有不足之處。最大的瓶頸就是迭代的次數比之前的演算法多了不少,如果遇到(10 000 000 000 000, 1)這類情況,就會相當地令人鬱悶了。
【解法三】
解法一的問題在於計算複雜的大整數除法運算,而解法二雖然將大整數的除法運算轉換成了減法運算,降低了計算的複雜度,但它的問題在於減法的迭代次數太多,那麼能否結合解法一和解法二從而使其成為一個最佳的演算法呢?答案是肯定的。
首先從分析公約數的特點入手:
對於y和x來說,如果y=k * y1,x=k * x1。那麼有f(y, x)= k * f(y1, x1)。
另外,如果x = p * x1,假設p是素數,並且y % p ! = 0(即y不能被p整除),那麼f(x, y)= f(p * x1, y)= f(x1, y)。
注意到以上兩點之後,我們就可以利用這兩點對演算法進行改進。
最簡單的方法是,我們知道,2是一個素數,同時對於二進位制表示的大整數而言,可以很容易地將除以2和乘以2的運算轉換成移位運算,從而避免大整數除法,由此就可以利用2這個數字來進行分析。
取p = 2
若x, y均為偶數,f(x, y)= 2 * f(x/2, y/2)= 2 * f(x>>1, y>>1)
若x為偶數,y為奇數,f(x, y)= f(x/2, y)= f(x>>1, y)
若x為奇數,y為偶數,f(x, y)= f(x, y/2)= f(x, y>>1)
若x, y均為奇數,f(x, y)= f(x, x - y),
那麼在f(x, y)= f(x, x - y)之後,(x - y)是一個偶數,下一步一定會有除以2的操作。
因此,最壞情況下的時間複雜度是O(log2(max(x, y))。
考慮如下的情況:
f(42, 30)= f(1010102, 111102)
= 2 * f(101012, 11112)
= 2 * f(11112, 1102)
= 2 * f(11112, 112)
= 2 * f(11002, 112)
= 2 * f(112, 112)
= 2 * f(02, 112)
= 2 * 112
= 6
根據上面的規律,具體程式碼實現如下:
程式碼清單2-16
BigInt gcd(BigInt x, BigInt y)
{
if(x < y)
return gcd(y, x);
if(y == 0)
return x;
else
{
if(IsEven(x))
{
if(IsEven(y))
return (gcd(x >> 1, y >> 1) << 1);
else
return gcd(x >> 1, y);
}
else
{
if(IsEven(y))
return gcd(x, y >> 1);
else
return gcd(y, x - y);
}
}
}
BigInt見解法二中的解釋,IsEven(BigInt x)函式檢查x是否為偶數,如果x為偶數,則返回true,否則返回false。
解法三很巧妙地利用移位運算和減法運算,避開了大整數除法,提高了演算法的效率。程式設計師常常將移位運算作為一種技巧來使用,最常見的就是通過左移或右移來實現乘以2或除以2的操作。其實移位的用處遠不止於此,如求一個整數的二進位制表示中1的個數問題(見本書2.1節“求二進位制數中1的個數”)和逆轉一個整數的二進位制表示問題等,往往讓人拍案叫絕