本博客原文地址:https://www.cnblogs.com/BobHuang/p/15498007.html,原文體驗更佳。
最大公約數在教材必修1 P38、P47以及選修1 P120出現,較為困難,其算法流程、迭代以及遞歸思想都是教材重難點,屬於較為經典的必學算法之一。
一、循環枚舉
什么是最大公約數呢?如果整數n除以m,得出結果是沒有余數的整數,就稱m是n的約數。A和B的最大公約數指A和B公共約數中最大的一個。
教材必修1 P91 整數的因子中介紹我們求一個整數x的因子可以一一列舉 \([1,x]\) 范圍內的所有整數,如果x能被這個范圍內的某個整數整除,那么這個數就是整數x的因子。
那我們解決TZOJ7380: 最大公約數之枚舉時,我們也可以一一枚舉其因子,最大公約數最小為1,最大公約數最大為數A和B中較小數,即枚舉范圍為 \([1,\min(A,B)]\) 。
倒着枚舉找到公因子即結束循環是比較方便的,參考代碼如下所示。
TZOJ7380參考代碼1
n=int(input())
m=int(input())
if n<m:
min_value = n
else:
min_value = m
for i in range(min_value,0,-1):
if n%i==0 and m%i==0:
print(i)
break
如果枚舉初始值為大數並不會影響程序結果,僅會增加循環次數,所以隨便選擇一個輸入的數作為枚舉初始值也是正確的。
TZOJ7380參考代碼2
n=int(input())
m=int(input())
for i in range(n,0,-1):
if n%i==0 and m%i==0:
print(i)
break
如果從小開始枚舉,記錄下答案最后輸出即可。
TZOJ7380參考代碼3
n=int(input())
m=int(input())
for i in range(1,n+1):
if n%i==0 and m%i==0:
ans=i
print(ans)
我們還可以分別預處理出A和B的因子,並且因子枚舉范圍縮小至 \(1\) 至 \(\lfloor{x/2} \rfloor\) ,然后再求出最大公因子。
無論采用以上哪種方法,我們循環枚舉求最大公約數的算法復雜度均為\(O(n)\),即我們需要 n*常數次 循環。
二、因數分解
在數學學習中,我們求最大公約數往往使用質因數分解的方法。
7382: 最大公約數之因數分解 就是這樣, 一個整數其質因數分解唯一,比如\(1008=2^4*3^2*7^1\),\(60=2^2\times3^1\times5^1\),他們的最大公約數可以把所有因數指數取小而得,即 \(2^2 \times 3^1 \times 5^0\times 7^0=12\) ,最小公倍數可以把所有因數指數取大獲得,如下圖所示。
我們如何求得質因數分解呢,我們可以找到其質因數,然后將其除盡並統計。這個過程可以和判斷素數一樣在\(\sqrt x\)次完成,其代碼已給出。
質因數分解代碼
# 計算質因數分解的字典
def cal_factor_dic(x):
dic={}
# 最小因數為2,最大為x**0.5,一一枚舉
for i in range(2,int(x**0.5)+1):
if x%i==0:
# 存在質因數i,t存儲有幾個
t=0
# 將其質因數除盡
while x%i==0:
t+=1
x=x//i
# 將質因數i及其個數存儲,質因數作為key,指數作為value
dic[i]=t
# 如果沒有除盡,存在x這個質因數
if x>1:
dic[x]=1
return dic
那如何將其公因數取小呢,我們可以循環A的因數,如果在B中也存在對指數取小即可,參考代碼如下。
TZOJ7382參考代碼
def greatest_common_divisor(a,b):
ans=1
for i in a:
if i in b:
ans=ans*i**min(a[i],b[i])
return ans
完成了可以嘗試下 TZOJ7383: 最小公倍數之因數分解
這個算法的復雜度為\(O(\sqrt n)\),cal_factor_dic()函數復雜度為\(O(\sqrt n)\),greatest_common_divisor()函數由於其質因子個數不超過 \(\log n\) 個,所以其復雜度為 \(O(\log n)\),綜合復雜度為\(2*O(\sqrt n)+O(\log n)\)。\(O(\log n)\)與\(O(\sqrt n)\)相比較小可以省略,並忽略常數即為這個算法的復雜度\(O(\sqrt n)\)。
三、更相減損術
7134: 最大公約數之更相減損術 這個算法來源於我們古代數學專著《九章算術》,其文言文描述為“可半者半之,不可半者,副置分母、子之數,以少減多,更相減損,求其等也。以等數約之。”
翻譯為現代語言如下:
1)任意給定兩個正整數,判斷它們是否都是偶數。若是,用2約簡;若不是,執行 2);
2)以較大的數減去較小的數,接着把所得的差與較小的數比較,並以大數減小數.繼續這個操作,直到所得的數相等為止,則這個數(等數)或這個數與約簡的數的乘積就是所求的最大公約數。
其算法程序流程圖如下所示
下圖可能對你了解這個算法有幫助,看完你可以嘗試自己寫出代碼並提交測試。兩條線段長分別可表示252和105,則其中每一小分段長代表最大公約數21。如動畫所示,只要輾轉地從大數中減去小數,直到其中一段的長度為0,此時剩下的一條線段的長度就是252和105的最大公因數。
7134參考代碼1
a=int(input())
b=int(input())
# 存儲有多少個2
k=1
# 第一步,求出多少個2
while a%2==0 and b%2==0:
a=a//2
b=b//2
k=2*k
# 第二步,更相減損
while a!=b:
if a>b:
a=a-b
else:
b=b-a
print(k*a)
第一步為算法優化,直接更相減損也可以,代碼如下所示。
7134參考代碼2
a=int(input())
b=int(input())
while a!=b:
if a>b:
a=a-b
else:
b=b-a
print(a)
當然我們也可以使用選修1的遞歸函數來解決這個問題。
更相減損遞歸代碼
def gcd(a,b):
if a==b:
return a
if a<b:
return gcd(a,b-a)
if a>b:
return gcd(a-b,b)
a=int(input())
b=int(input())
print(gcd(a,b))
Python對遞歸次數有限制,此代碼遞歸調用次數太多,故無法通過此題。
更相減損代碼已比較簡潔,但其算法復雜度並沒有實質性的變化,如果最大公約數為1,需要運行n次,故其復雜度為\(O(n)\)。
四、輾轉相除法
7135: 最大公約數之輾轉相除法 輾轉相除法是更相減損的一個優化,將其減的過程轉換為了取余,算法流程如下:
1)輸入兩個正整數m和n。
2)若m<n,則交換m和n的值。
3)若m除以n,相除得到的余數r。
4)若r=0,則輸出n的值,算法結束;否則,執行步驟5)。
5)令m=n,n=r,返回步驟3)繼續執行。
算法流程圖如下所示,你可以嘗試自己寫出代碼並提交測試。
我們用死循環模擬其步驟,參考代碼如下所示。
7135參考代碼1
n=int(input())
m=int(input())
# 死循環
while True:
# m<n交換
if m<n:
m,n=n,m
# 求出取余值
r=m%n
# 為0代表找到了
if r==0:
break
# 執行第5步,縮小范圍
m,n=n,r
print(n)
上述死循環可以使用迭代公式\(gcd(m,n)=gcd(n,m \mod n)\)優化,參考代碼如下所示。
7135參考代碼2
def gcd(m,n):
while n!=0:
temp=n
n=m%n
m=temp
return m
n=int(input())
m=int(input())
print(gcd(n,m))
也可以寫為遞歸形式,參考代碼如下所示。
7135參考代碼3
def gcd(a,b):
if b==0:
return a
return gcd(b,a%b)
n=int(input())
m=int(input())
print(gcd(n,m))
使用 if表達式 可以將此函數精簡函數為1行,參考代碼如下所示。
7135參考代碼4
def gcd(a,b):
return a if b==0 else gcd(b,a%b)
n=int(input())
m=int(input())
print(gcd(n,m))
輾轉相除法的算法復雜度為\(O(\log n)\),也是我們常用的求最大公約數方法。這個簡單數論在密碼學RSA算法也有運用,將其擴展可以求解不定方程、線性同余方程、逆元等,感興趣的同學歡迎大學選擇計算機類專業參加算法競賽。