來談談貪心算法

前言

以前講了動態規劃,在翻閱資料的時候看到了很多談論貪心算法的,這兩種算法也頗有類似之處,正好最近又作到了有關貪心的題,因此今天寫篇文章來談一談。python

貪心算法(英語:greedy algorithm),又稱貪婪算法,是一種在每一步選擇中都採起在當前狀態下最好或最優(即最有利)的選擇,從而但願致使結果是最好或最優的算法。
貪心算法在有最優子結構的問題中尤其有效。最優子結構的意思是局部最優解能決定全局最優解。簡單地說,問題可以分解成子問題來解決,子問題的最優解能遞推到最終問題的最優解。
貪心算法與動態規劃的不一樣在於它對每一個子問題的解決方案都作出選擇,不能回退。動態規劃則會保存之前的運算結果,並根據之前的結果對當前進行選擇,有回退功能。
貪心法能夠解決一些最優化問題,如:求圖中的最小生成樹、求哈夫曼編碼……對於其餘問題,貪心法通常不能獲得咱們所要求的答案。一旦一個問題能夠經過貪心法來解決,那麼貪心法通常是解決這個問題的最好辦法。因爲貪心法的高效性以及其所求得的答案比較接近最優結果,貪心法也能夠用做輔助算法或者直接解決一些要求結果不特別精確的問題。
——摘自維基百科

動態規劃和貪心算法很像,在各類對它們的描述中都有將問題分解爲子問題的說法,其實還有分治法也是這種模式。可是動態規劃實質上是窮舉法,只是會省去重複計算,而貪心算法,正如它的名字,貪心,每次都選擇局部的最優解,並不考慮這個局部最優選擇對全局的影響。
能夠說貪心算法是動態規劃的一種特例,也正因爲貪心算法只考慮子問題的最優解,能夠說,貪心算法實際上能解決的問題有限,它是一個目光短淺的算法,只考慮當下,只有當這種基於局部最優的選擇最終能致使總體最優解的情形才能用貪心算法來解決。算法

仍是舉個栗子

一塊兒來看一下一道leetcode上的題:數組

假設你是一位很棒的家長,想要給你的孩子們一些小餅乾。可是,每一個孩子最多隻能給一塊餅乾。對每一個孩子 i ,都有一個胃口值 gi ,這是能讓孩子們知足胃口的餅乾的最小尺寸;而且每塊餅乾 j ,都有一個尺寸 sj 。若是 sj >= gi ,咱們能夠將這個餅乾 j 分配給孩子 i ,這個孩子會獲得知足。你的目標是儘量知足越多數量的孩子,並輸出這個最大數值。
注意:
你能夠假設胃口值爲正。
一個小朋友最多隻能擁有一塊餅乾。
來源:力扣(LeetCode)
連接: https://leetcode-cn.com/probl...
著做權歸領釦網絡全部。商業轉載請聯繫官方受權,非商業轉載請註明出處。

是的,這是一位很棒(摳門)的家長,要儘量用少的餅乾知足多的孩子。好比如今有三個孩子胃口是[1,2,3],那麼哪怕家長手上有一百塊尺寸爲1的小餅乾,也只能知足一個孩子,由於他每一個孩子最多隻給一個餅乾。
讓咱們來想想如何「貪心」呢?
要想最節省餅乾,咱們能夠把餅乾尺寸孩子胃口這兩個數據先作一下升序排序,而後每次都用最小的餅乾去試試可否知足胃口最小的孩子,這樣咱們須要維護兩個索引。微信

代碼實現:

class Solution:
    def findContentChildren(self, g: List[int], s: List[int]) -> int:
        count = 0
        g.sort()
        s.sort()
        gi, si = 0, 0
        while gi < len(g) and si < len(s):
            if s[si] >= g[gi]:
                count += 1
                gi += 1
                si += 1
            elif s[si] < g[gi]:
                si += 1
        return count

當餅乾尺寸恰好大於等於孩子胃口,計數+1,兩個索引值+1,不然,餅乾尺寸列表索引+1,看看更大的那塊餅乾可否知足當前孩子。
題外話:常常看到有的Python代碼中,將某個列表長度值保存到某個變量中,像size = len(alist)這樣,事實上len()函數花費的是O(1)常數時間。Python的設計中一切皆對象,列表固然也是對象,當你建立一個列表後,len()實質上只是去提取了這個列表實例的長度屬性值而已,並無遍歷列表之類的操做。cookie

實踐

再來看個題目:網絡

在一條環路上有 N 個加油站,其中第 i 個加油站有汽油 gas[i] 升。
你有一輛油箱容量無限的的汽車,從第 i 個加油站開往第 i+1 個加油站須要消耗汽油 cost[i] 升。你從其中的一個加油站出發,開始時油箱爲空。
若是你能夠繞環路行駛一週,則返回出發時加油站的編號,不然返回 -1。
說明: 
若是題目有解,該答案即爲惟一答案。
輸入數組均爲非空數組,且長度相同。
輸入數組中的元素均爲非負數。

首先咱們能夠想到,有一種狀況,是必定不可能跑徹底程的,那就是加油站的油量總和小於路上消耗的總油量時。也就是說,若是sum(gas) < sum(cost),那麼就要返回-1
第二點,若是咱們選擇一個加油站i爲起始點,若是這個加油站所可以得到的油量小於前往下一個加油站所花費的油量,也就是gas[i] < cost[i]的話,說明這個加油站不能作爲起點。函數

代碼實現:

class Solution:
    def canCompleteCircuit(self, gas: List[int], cost: List[int]) -> int:
        total, curr = 0, 0
        start = 0
        for i in range(len(gas)):
            total += gas[i] - cost[i]
            curr += gas[i] - cost[i]
            if curr < 0:
                start = i + 1
                curr = 0

        return start if total >= 0 else -1

這裏用total保存最終的油量,curr表示當前油箱油量,start表示起點,初值都設爲0,遍歷整個列表,若是在加油站i,gas[i] - cost[i] < 0,那麼就選擇第i+1個加油站作爲起點,最後若是total小於0,返回-1,不然就返回start優化

  • 時間複雜度:O(n)
  • 空間複雜度:O(1)

看看缺陷

如下例子來自知乎用戶@阮行止:ui

先來看看生活中常常遇到的事吧——假設您是個土豪,身上帶了足夠的一、五、十、20、50、100元面值的鈔票。如今您的目標是湊出某個金額w,須要用到儘可能少的鈔票。  依據生活經驗,咱們顯然能夠採起這樣的策略:能用100的就儘可能用100的,不然儘可能用50的……依次類推。在這種策略下,666=6×100+1×50+1×10+1×5+1×1,共使用了10張鈔票。  這種策略稱爲「貪心」:假設咱們面對的局面是「須要湊出w」,貪心策略會盡快讓w變得更小。能讓w少100就儘可能讓它少100,這樣咱們接下來面對的局面就是湊出w-100。長期的生活經驗代表,貪心策略是正確的。  可是,若是咱們換一組鈔票的面值,貪心策略就也許不成立了。若是一個奇葩國家的鈔票面額分別是一、五、11,那麼咱們在湊出15的時候,貪心策略會出錯:  15=1×11+4×1 (貪心策略使用了5張鈔票)  15=3×5 (正確的策略,只用3張鈔票)
做者:阮行止
連接: https://www.zhihu.com/questio...
來源:知乎
著做權歸做者全部。商業轉載請聯繫做者得到受權,非商業轉載請註明出處。

能夠看到,在第一種狀況下,使用貪心策略,很快就能得出答案,可是當條件稍微改變,就沒法得出正確答案了。貪心算法在這個問題中,每次都選擇面額最大的鈔票,快速減小了最終要湊出的W的量,可是在例子的特殊狀況裏,第一次選擇最大的面額11的鈔票,會致使後面只能選擇4張1元鈔票,最終獲得的解是不正確的。
能夠說,動態規劃是在暴力枚舉的基礎上,避免了重複計算,可是每個子問題都被考慮到了,而貪心算法則每次都短視的選擇當前最優解而不去考慮剩下的狀況。
最後留個思考,試試把這個特殊面額鈔票的問題用動態規劃解決一下。編碼

掃碼關注微信公衆號:
公衆號

相關文章
相關標籤/搜索