python常用演算法(6)——貪心演算法,歐幾里得演算法
1,貪心演算法
貪心演算法(又稱貪婪演算法)是指,在對問題求解時,總是做出在當前看來是最好的選擇。也就是說,不從整體最優上加以考慮,他所做出的的時在某種意義上的區域性最優解。
貪心演算法並不保證會得到最優解,但是在某些問題上貪心演算法的解就是最優解。要會判斷一個問題能否用貪心演算法來計算。貪心演算法和其他演算法比較有明顯的區別,動態規劃每次都是綜合所有問題的子問題的解得到當前的最優解(全域性最優解),而不是貪心地選擇;回溯法是嘗試選擇一條路,如果選擇錯了的話可以“反悔”,也就是回過頭來重新選擇其他的試試。
1.1 找零問題
假設商店老闆需要找零 n 元錢,錢幣的面額有100元,50元,20元,5元,1元,如何找零使得所需錢幣的數量最少?(注意:沒有10元的面額)
那要是找376元零錢呢? 100*3+50*1+20*1+5*1+1*1=375
程式碼如下:
# t表示商店有的零錢的面額 t = [100, 50, 20, 5, 1] # n 表示n元錢 def change(t, n): m = [0 for _ in range(len(t))] for i, money in enumerate(t): m[i] = n // money # 除法向下取整 n = n % money # 除法取餘 return m, n print(change(t, 376)) # ([3, 1, 1, 1, 1], 0)
1.2 揹包問題
常見的揹包問題有整數揹包和部分揹包問題。那問題的描述大致是這樣的。
一個小偷在某個商店發現有 n 個商品,第 i 個商品價值 Vi元,重 Wi 千克。他希望拿走的價值儘量高,但他的揹包最多隻能容納W千克的東西。他應該拿走那些商品?
0-1揹包:對於一個商品,小偷要麼把他完整拿走,要麼留下。不能只拿走一部分,或把一個商品拿走多次(商品為金條)
分數揹包:對於一個商品,小偷可以拿走其中任意一部分。(商品為金砂)
舉例:
對於 0-1 揹包 和 分數揹包,貪心演算法是否都能得到最優解?為什麼?
顯然,貪心演算法對於分數揹包肯定能得到最優解,我們計算每個物品的單位重量的價值,然後將他們降序排序,接著開始拿物品,只要裝得下全部的該類物品那麼就可以全裝進去,如果不能全部裝下就裝部分進去直到揹包裝滿為止。
而對於此問題來說,顯然0-1揹包肯定裝不滿。即使偶然可以,但是也不能滿足所有0-1揹包問題。0-1揹包(又叫整數揹包問題)還可以分為兩種:一種是每類物品數量都是有限的(bounded)。一種是數量無限(unbounded),也就是你想要的多少有多少,這兩種都不能使用貪心策略。0-1揹包是典型的第一種整數揹包問題。
分數揹包程式碼實現:
# 每個商品元組表示(價格,重量) goods = [(60, 10), (100, 20), (120, 30)] # 我們需要對商品首先進行排序,當然這裡是排好序的 goods.sort(key=lambda x: x[0]/x[1], reverse=True) # w 表示揹包的容量 def fractional_backpack(goods, w): # m 表示每個商品拿走多少個 total_v = 0 m = [0 for _ in range(len(goods))] for i, (prize, weight) in enumerate(goods): if w >= weight: m[i] = 1 total_v += prize w -= weight # m[i] = 1 if w>= weight else weight / w else: m[i] = w / weight total_v += m[i]*prize w = 0 break return m, total_v res1, res2 = fractional_backpack(goods, 50) print(res1, res2) # [1, 1, 0.6666666666666666]
1.3 拼接最大數字問題
有 n 個非負數,將其按照字串拼接的方式拼接為一個整數。如何拼接可以使得得到的整數最大?
例如:32, 94, 128, 1286, 6, 71 可以拼接成的最大整數為 94716321286128.
注意1:字串比較數字大小和整數比較數字大小不一樣!!! 字串比較大小就是首先看第一位,大的就大,可是一個字串長,一個字串短如何比較呢?比如128和1286比較
思路如下:
# 簡單的:當兩個等位數相比較 a = '96' b = '97' a + b if a > b else b + a # 當出現了下面的不等位數相比較,如何使用貪心演算法呢? # 我們轉化思路,拼接字串,比較結果 a = '128' b = '1286' # 字串相加 a + b = '1281286' b + a = '1286128' a + b if a + b > b + a else b + a
數字拼接程式碼如下:
from functools import cmp_to_key li = [32, 94, 128, 1286, 6, 71] def xy_cmp(x, y): # 其中1表示x>y,-1,0同理 if x+y < y+x: return 1 elif x+y > y+x: return -1 else: return 0 def number_join(li): li = list(map(str, li)) li.sort(key=cmp_to_key(xy_cmp)) return "".join(li) print(number_join(li)) # 94716321286128
1.4 活動選擇問題
假設有 n 個活動,這些活動要佔用同一片場地,而場地在某時刻只能供一個活動使用。
每一個活動都有一個開始時間 Si 和結束時間 Fi (題目中時間以整數表示)表示活動在 [Si, fi) 區間佔用場地。(注意:左開右閉)
問:安排哪些活動能夠使該場地舉辦的活動的個數最多?
貪心結論:最先結束的活動一定是最優解的一部分。
證明:假設 a 是所有活動中最先結束的活動,b是最優解中最先結束的活動。
如果 a=b,結論成立
如果 a!=b,則 b 的結束時間一定晚於 a 的結束時間,則此時用 a 替換掉最優解中的 b ,a 一定不與最優解中的其他活動時間重疊,因此替換後的解也是最優解。
程式碼如下:
# 一個元組表示一個活動,(開始時間,結束時間) activities = [(1, 4), (3, 5), (0, 6), (5, 7), (3, 9), (5, 9), (6, 10), (8, 11), (8, 12), (2, 14), (12, 16)] # 保證活動是按照結束時間排好序,我們可以自己先排序 activities.sort(key=lambda x:x[1]) def activity_selection(a): # 首先a[0] 肯定是最早結束的 res = [a[0]] for i in range(1, len(a)): if a[i][0] >= res[-1][1]: # 當前活動的開始時間小於等於最後一個入選活動的結束時間 # 不衝突 res.append(a[i]) return res res = activity_selection(activities) print(res)
1.5 最大子序和
求最大子陣列之和的問題就是給定一個整數陣列(陣列元素有負有正),求其連續子陣列之和的最大值。下面使用貪心演算法逐個遍歷。
程式碼如下:
def maxSubarray(li): s_max, s_sum = 0, 0 for i in range(len(li)): s_sum += li[i] s_max = max(s_max, s_sum) if s_sum < 0: s_sum = 0 return s_max
2,歐幾里得演算法——最大公約數
2.1,最大公約數的定義
約數:如果整數 a 能被整數 b 整除,那麼 a 叫做 b 的倍數,b 叫做 a 的約數。
最大公約數(Greatest Common Divisor):給定兩個整數 a, b,兩個數的所有公共約數中的最大值即為最大公約數。
例如:12和16的最大公約數是 4 。
2.2,歐幾里得演算法如下:
歐幾里得演算法又稱為輾轉相除法,用於計算兩個正整數a,b的最大公約數。
- E:設兩個正整數a, b,且已知a>b
- E1:令r = a%b('%'代表取餘)
- E2:若r=0(即n整除m),結束運算,n即為結果
- E3:否則令a=b,b=r,並返回步驟E1
歐幾里得演算法運用了這樣一個等價式(設 gcd(a, b)代表 a 和 b 的最大公約數,mod()代表取餘運算或模運算)則:
gcd(a, b) = gcd(b, a mod b ) = gcd(b, a%b)
也就是說 m , n 的最大公約數等於他們相除餘數(r)和 n 的最大公約數。
例如:gcd(60, 21) = gcd(21, 18) = gcd(18, 3) = gcd(3, 0) = 3
意思就是 60對21取餘18,同理21對18餘3,18對3取餘0,所以3為兩個數的最大公約數。
2.3,證明歐幾里得公式
我們的證明分為兩步。第一步,證明gcd(a, b)是b, a%b 的一個公約數。第二步,證明這個公約數是最大的。
1,證明gcd(a, b)是b, a%b 的一個公約數
1,因為任意兩個正整數都有最大公因數,設為 d。
2,將 a , b 分別用最大公因數 d 來表示為 a = k1*d b = k2*d (k1,k2是兩個常數)
3,設 a = k*b + c (也就是a 除以 b 商 k 餘 c),然後把a = k1*d b = k2*d 兩個式子中的 a,b代入式子,得到:
c = a - k*b = k1*d - k * k2 * d,然後再提取公因數 d,得到 c = (k1 - k2 * k)*d,這就說明,c也就是 a%b有 d 這個約數,因為開始我們設 任意兩個數都有最大公約數d,所以 gcd(a, b) 是 b, a%b 的一個公約數。
4,由此可以得到 c 是最大公因數 d 的倍數,得證:gcd(a, b) = gcd(b, a mod b)。所以以此類推,可以將 m n中較大的數用較小的數的餘數 r 替換,實現了降維,所以有了E3的步驟。
2,證明我們求出來的公約數是最大的
1,數學是一門嚴謹的學科,我們需要嚴謹的正面,我們知道 c(a%b) = k1*d - k * k2 * d b = k2*d,所以我們只需要證明k1-k*k2, k2互質即可。
2,這裡可以用到反證法,我們假設 k1 - k*k2 = q*t k2=p*t,再講這個k1 代入最開始的 a=k1*d ,得到 a=(q*t + k*k2)*d,再利用乘法分配律得到: a = q*t*d + k*k2*d,這時候我們發現,k2*d就是b,將其代入,得到 a=q*t*d + b*d
3,我們在將k2 = p*t代入開始的b = k2*d,得到b = p*t*d,再把這個式子代到a = q*t*d+b*d.得到了:a = q*t*d+p*t*d.提取公因數:a=(q+p)*t*d
4,再和b=p*t*d比較,發現他們的最大公因數變成了t*d和開始矛盾,所以假設不成立,反證成功!
2.4,如何計算最大公約數?
1,歐幾里得:輾轉相除法(歐幾里得演算法)
2,《九章算術》:更相減損術
程式碼如下:
# 遞迴法:保證a>b def gcd(a, b): if b == 0: return a else: return gcd(b, a % b) # 遞推法 def gcd1(a, b): if a < b: a, b = b, a while b > 0: r = a % b a = b b = r return a
因為這是一個偽遞迴,所以時間複雜度不高。
2.5,應用:實現分數計算
利用歐幾里得演算法實現一個分數類,支援分數的四則運算。
程式碼如下:
# _*_coding:utf-8_*_ class Fraction: def __init__(self, a, b): self.a = a self.b = b x = self.gcd(a, b) self.a /= x self.b /= x # 最大公約數 def gcd(self, a, b): while b > 0: r = a % b a = b b = r return a # 最小公倍數 def zgs(self, a, b): # 12 16 -> 4 # 3 * 4 * 4=48 x = self.gcd(a, b) return (a * b / x) # 加法的內建方法 def __add__(self, other): # 1/12 + 1/20 a = self.a b = self.b c = other.a d = other.b fenmu = self.zgs(b, d) femzi = a * (fenmu / b) + c * (fenmu / d) return Fraction(femzi, fenmu) def __str__(self): return "%d/%d" % (self.a, self.b) f = Fraction(30, 16) print(f)
2.7 歐幾里得演算法的缺點
歐幾里得演算法是計算兩個數最大公約數的傳統演算法,無論從理論還是實際效率上都是很好地。但是卻有一個致命的缺陷,這個缺陷在素數比較小的時候一般是感受不到的,只有在大素數時才會顯現出來。
一般實際應用中的整數很少會超過64位(當然現在已經允許128位),對於這樣的整數,計算兩個數之間的模很簡單。對於字長為32位的平臺,計算兩個不超過32位的整數的模,只需要一個指令週期,而計算64位以下的整數模,也不過幾個週期而已。但是對於更大的素數,這樣的計算過程就不得不由使用者來設計,為了計算兩個超過64位的整數的模,使用者也許不得不採用類似於多位數除法手算過程中的試商法,這個過程不但複雜,而且消耗了很多CPU時間。對於現代密碼演算法,要求計算128位以上的素數的情況比比皆是,設計這樣的程式迫切希望能夠拋棄除法和取模。
由J. Stein 1961年提出的Stein演算法很好的解決了歐幾里德演算法中的這個缺陷,Stein演算法只有整數的移位和加減法,為了說明Stein演算法的正確性,首先必須注意到以下結論:
gcd(a,a)=a,也就是一個數和其自身的公約數仍是其自身。 gcd(ka,kb)=k gcd(a,b),也就是最大公約數運算和倍乘運算可以交換。特殊地,當k=2時,說明兩個偶數的最大公約數必然能被2整除。 當k與b互為質數,gcd(ka,b)=gcd(a,b),也就是約掉兩個數中只有其中一個含有的因子不影響最大公約數。特殊地,當k=2時,說明計算一個偶數和一個奇數的最大公約數時,可以先將偶數除以2。程式碼如下:
def gcd_Stein(a, b): if a < b: a, b = b, a if (0 == b): return a if a % 2 == 0 and b % 2 == 0: return 2 * gcd_Stein(a/2, b/2) if a % 2 == 0: return gcd_Stein(a / 2, b) if b % 2 == 0: return gcd_Stein(a, b / 2) return gcd_Stein((a + b) / 2, (a - b) / 2)
參考文獻:https://www.cnblogs.com/jason2003/p/9797750.html
https://www.cnblogs.com/Dragon5/p/6401596.html