在上一篇博客中,咱們經過選擇問題瞭解了貪心算法。這一篇博客將繼續介紹貪心算法,主要談談貪心算法的原理,並簡單分析一下揹包問題。html
經過上一篇博客中的選擇問題,咱們看到,貪心算法能夠由以下幾個步驟來實現:python
對比動態規劃,咱們發現貪心算法和它十分類似,首先它們都必須具有最優子結構性質,而後一般都是將原問題分解爲子問題,根據最優子結構性質與問題的分解,設計一個遞歸算法。不一樣之處在於,動態規劃算法在對問題進行分解時,因爲沒法肯定哪種分解可以獲得原問題的最優解,所以須要考察全部的分解狀況,而且正是因爲這種分解問題的不肯定性,一般會致使子問題重疊,爲了提升效率,一般會用一個「備忘錄」去備忘子問題的解;而在貪心算法中,咱們很明確的知道如何分解問題能產生最優解(或者說是很明確的知道哪一種選擇的結果在最優解中),所以一般貪心算法沒有子問題重疊重疊問題,但咱們必需要確保貪心選擇的正確性。算法
和動態規劃同樣,咱們也總結出貪心算法的兩個特色(必要條件)。安全
這個性質和動態規劃同樣,所以再也不贅述:測試
若是一個問題的最優解包含其子問題的最優解,咱們就稱此問題具備最優子結構性質。spa
對於某個問題,若是咱們能夠經過作出局部最優(貪心)選擇來構造全局最優解,那麼咱們就稱該問題具備貪心選擇性質。設計
下面經過兩種不一樣形式的揹包問題,來進一步說明動態規劃算法與貪心算法的區別之處,進而加深對貪心算法的理解。code
首先說明一下揹包問題:htm
給定一組物品和一個限定重量的揹包,每種物品都有本身的重量和價格。在限定的總重量內,咱們如何選擇,才能使得揹包內的物品總價格最高。blog
在0-1揹包問題中,對於每一件物品,你只能作出二元(0-1)選擇,即只能選擇 選擇該物品或不選擇該物品,而不能只選擇該物品的一部分;分數揹包問題相反。
咱們先作以下說明:
設共有\(n\)件物品,記爲\(t_1,t_2,...,t_n\),其中物品\(t_i\)重量爲\(w_i\),價值爲\(p_i\);限重爲\(W\)。
首先,咱們可證實,0-1揹包問題具備最優子結構性質:
由於,對於某種最優選擇方案,假設\(t_i\)屬於其中,若是咱們拿走\(t_i\),那麼原問題則變爲子問題:從商品\(t_1, ...,t_{i-1},t_{i+1}, ...,t_n\)中選擇某些物品,限重爲\(W-p_i\)。在子問題的全部選擇方案中,要想讓其中的某種方案與\(t_i\)組合成原問題的一個最優方案,該方案必須是子問題的一個最優方案。用剪切-粘貼法能夠很容易證實,再也不贅述。
根據上面的分析,咱們先這麼考慮,設\(P_{T, w}\)爲物品集合爲\(T\),限重\(w\)時,選擇的物品的最高總價值,咱們能夠很容易用一個遞歸式去表示最優解:
\[ P_{T, w} = \begin {cases} 0 & \text{若$T = \{t_i\}, w_i > w$ }\\ p_i&\text{若$T = \{t_i\}, w_i \leq w$}\\ \max\limits_{t_i \in T, w_i \leqslant w}(p_i + P_{T-t_i, w-w_i})&\text{其餘} \end{cases} \]
下面給出此遞歸式的Python實現:
def knapsack_0_1(T, w): if len(T) == 1: if T[0][2] <= w: return T[0][1] return 0 maxValue = 0 for i, t in enumerate(T): if t[2] <= w: value = t[1] + knapsack_0_1(T[:i] + T[i + 1:], w - T[i][2]) if maxValue < value: maxValue = value return maxValue
咱們做以下測試:
if __name__ == '__main__': ''' 1號商品:$60 - 10kg 2號商品:$100 - 20kg 3號商品:$120 - 30kg 限重 50kg ''' T = [(1, 60 , 10), (2, 100, 20), (3, 120, 30)] W = 50 print(knapsack_0_1(T, W))
打印結果:220
從新審視上述遞歸式和以上的實現代碼,咱們發現其壓根就不是動態規劃算法,而只是簡單的遞歸算法。由於它不知足動態規劃問題的一個必要條件:子問題重疊。實際上它也沒有充分利用最優子結構性質,而致使「重複」求解了許多問題。其時間複雜度爲\(O(n!)\)。
要用上動態規劃算法,咱們能夠這麼去考慮,設\(P[i, w]\)表示物品\(t_1, ...,t_i\)在限重爲\(w\)時,可以選擇的物品的最高價值。咱們能夠用以下遞歸式去表示\(P[i, w]\):
\[ P[i,w] = \begin {cases} 0 & \text{$w=0$或$i=0$}\\ P[i-1, w] & \text{$w_i > w, i = 1, 2,..., n $}\\ \max\limits_{w_i \leq w} \{P[i-1, w], p_i + P[i-1, w-w_i]\} & \text{$i = 1, 2,..., n$} \end{cases} \]
簡單解釋一下上述遞歸式:當\(w = 0\)或\(i = 0\)時,顯然\(P[i, w] = 0\);當\(w_i > w\)時,由於沒法將物品\(t_i\)放入揹包(即不能選擇\(t_i\)),所以\(P[i, w] = P[i-1, w]\);第三種狀況,既能夠選擇\(t_i\)也能夠不選擇\(t_i\),所以須要在這兩者之間找出最大值。咱們的目標是求出\(P[n, W]\)。
下面給出一個自底向上的Python實現代碼:
def knapsack_0_1(T, w): P = [[0] * (w + 1)for i in range(len(T) + 1)] for i, t in enumerate(T): i = i + 1 # i從0開始迭代,所以必須加上1 for j in range(1, w + 1): if t[2] > j: P[i][j] = P[i - 1][j] else: P[i][j] = max(P[i - 1][j], t[1] + P[i-1][j - t[2]]) return P
咱們作一樣的測試:
if __name__ == '__main__': ''' 1號商品:$60 - 10kg 2號商品:$100 - 20kg 3號商品:$120 - 30kg 限重 50kg ''' # 注意:下面咱們將重量都同步縮減爲原來的0.1倍,不影響結果。 T = [(1, 60 , 1), (2, 100, 2), (3, 120, 3)] W = 5 print(knapsack_0_1(T, W)[len(T)][W])
打印結果爲:220
分析上述動態規劃算法,咱們發現其時間複雜度爲:\(O(n \times W)\),比一開始的遞歸算法要好。
分數揹包同0-1揹包同樣,也具備最優子結構,其證實和0-1揹包差很少,這裏再也不贅述。
在分數揹包問題中,直覺告訴咱們,這樣作可以總價值最高的商品:首先用平均價值最高的商品去填充揹包。若揹包限重還有剩餘,則用平均價值第二高的商品去填充揹包……以後的狀況以此類推,直到揹包被「填滿」。先提早聲明,揹包必定是能被「填滿」的,即所給的商品的總重量是大於揹包的限重的;對於揹包限重大於或等於商品總重量的「平凡」狀況,沒有考慮的必要。
以上的選擇策略即是該問題的貪心選擇。接下來咱們必須證實貪心選擇是安全的。
考慮在某種最優選擇方案中,在第\(k\)次選擇時,\(t_i\)是當前所剩的商品中平均價值最高的商品。假設在該最優方案的該次選擇中,選擇的商品\(t_j\)不是平均價值最高的商品,即\(j \neq i\)。咱們能夠採用剪切-粘貼法,即考慮用\(t_i\)去替代\(t_j\)(若\(w_i \geq w_j\),則取重量爲\(w_j\)的部分\(t_i\)去替代;若\(w_i < w_j\),則取所有的\(t_i\)去替代 ),很明顯,替代後的方案比以前的方案更優。所以假設不成立,即\(j = i\),即在第\(k\)次選擇時,選擇的商品\(t_j\)是剩餘商品中平均價值最高的商品,因爲\(k\)具備任意性,所以貪心選擇是安全的。
有了上述的分析,咱們能夠很容易設計出一個貪心算法,首先將全部商品按平均價值遞減的順序排序,而後再採用如上貪心選擇策略作出選擇,這樣能夠在\(O(n\lg n)\)的時間內解決分數揹包問題。
下面給出Python實現代碼:
def knapsack_fraction(T, w): T.sort(key = lambda t: t[1] / t[2], reverse= True) p = 0 for t in T: if w <= t[2]: p += w * t[1] / t[2] return p else: p += t[1] w -= t[2]
做以下測試:
if __name__ == '__main__': ''' 1號商品:$60 - 10kg 2號商品:$100 - 20kg 3號商品:$120 - 30kg 限重 50kg ''' T = [(1, 60 , 10), (2, 100, 20), (3, 120, 30)] W = 50 print(knapsack_fraction(T, W))
打印:240.0
你可能會想,爲何不把用於分數揹包問題的貪心算法也用於0-1呢?事實上,是不行的。從咱們測試的例子中,就能看出問題。
在分數揹包中,最優方案揹包裏的物品是:10kg的1號商品,20kg的2號商品和20kg的3號商品。注意此時3號商品只取了20kg,它被「分割」了,而在0-1揹包問題中,這種「分割」是不容許的。
換個角度說,若是咱們對0-1揹包問題一樣採用如上貪心算法,而且還要保證不能「分割」物品,那麼最終咱們只能將10kg的1號商品,20kg的2號商品,其總價值爲$160,揹包還有20kg的載重空間被白白浪費了。
再換個角度,若是把上面的對分數揹包貪心選擇安全性的證實套用到0-1揹包問題中,咱們就會發現,在證實中用\(t_i\)去替代\(t_j\)的作法不必定可以成功,緣由仍是由於0-1揹包問題中,商品是不容許"切割"的,其重量老是一個固定值。