10.6 貪心算法詳解及LeetCode題目

可參考幾篇博客

詳解貪心算法(Python實現貪心算法典型例題)

五大常用算法之一:貪心算法

 

算法概述

貪心算法(又稱貪婪算法)是指,在對問題求解時,總是做出在當前看來是最好的選擇。也就是說,不從整體最優上加以考慮,所做出的是在某種意義上的局部最優解。

貪心算法不是對所有問題都能得到整體最優解,關鍵是貪心策略的選擇,選擇的貪心策略必須具備無後效性,即某個狀態以前的過程不會影響以後的狀態,只與當前狀態有關。

最重要的一點,動態規劃問題我們強調算法框架,然鵝貪心算法沒有固定的算法框架,算法設計的關鍵是貪心策略的選擇,具體問題具體分析,我們通過幾個問題理解算法設計思想。

 

算法特點

貪心選擇是指所求問題的整體最優解可以通過一系列局部最優的選擇,即貪心選擇來達到。這是貪心算法可行的第一個基本要素,也是貪心算法與動態規劃算法的主要區別。貪心選擇是採用從頂向下、以迭代的方法做出相繼選擇。

如何確定可用貪心算法?其實這並不是一個容易確定的問題。

需要證明每一步所作的貪心選擇最終能得到問題的最優解。通常可以首先證明問題的一個整體最優解,是從貪心選擇開始的,而且作了貪心選擇後,原問題簡化爲一個規模更小的類似子問題。然後,用數學歸納法證明,通過每一步貪心選擇,最終可得到問題的一個整體最優解。這是常用的數學歸納法。

第二個辦法是證明在接近問題目標的過程中貪心算法每一步的選擇至少不比任何其他算法差。第三個辦法是基於算法的輸出來證明。這裏不做解釋了,可參考《算法設計與分析基礎》。

 

算法思路

  • 建立數學模型來描述問題
  • 把求解的問題分成若干個子問題
  • 對每個子問題求解,得到子問題的局部最優解
  • 把子問題的解局部最優解合成原來問題的一個解

貪婪算法建議通過一系列步驟來構造問題的解,每一步對目前構造的部分解做一個擴展,直到獲得問題的完整解爲止。

這個技術的核心是,所做的每一步選擇都必須滿足以下條件:

  • 可行性(feasible)即必須滿足問題的約束
  • 局部最優(locally optimal)即它是當前步驟中所有可行的選擇中最優的
  • 不可取消(irrevocable)即一旦做出選擇在算法後面步驟中就無法改變了

以上引自《算法設計與分析基礎》,感覺用語十分準確。

我們再談最優子結構問題,動態規劃和貪心算法都是最優子結構。我們需要辨析二者的區別。

貪心算法對每個子問題的解決方案都做出最佳選擇併產生整個問題的最優解,f(i)及其以前的選擇都不再改變,不能回退;動態規劃則會根據以前的選擇結果對當前進行選擇,f(i+1)的選擇還可以根據f(1),f(2)...f(i)的結果,畢竟所有結果都可以記憶起來,這也是動態規劃算法的核心,因此DP有回退功能。

動態規劃主要運用於二維或三維問題,而貪心一般是一維問題 。

 

算法應用

這裏,我們簡要介紹貪心思想的幾個經典算法。

Prim算法和Kruskal算法:這是圖算法中最小生成樹(minimun spanning tree)的兩種解決方法,Prim算法在構造最小生成樹時,每一輪,把不在樹中的頂點,貪婪的包含進來,只選擇一個最優的(最近的)頂點,如此迭代n-1 輪(n爲頂點數),最終培養起一棵最小生成樹。算法的關鍵是貪婪策略的選擇,即每一次迭代時選擇的頂點的策略。Kruskal算法,也是解決這一問題的,它的貪婪策略的設計完全不同,首先按照權重對邊進行非遞減排序,然後每一輪迭代,試圖將下一條邊加進來,可以不連通但是不可有環。算法細節不再介紹,理解對同一問題的不同貪婪策略的設計思想。

Dijkstra算法,用於解決加權圖的單起點最短路徑問題,其貪心策略的設計與Prim相似,不同的是它比較的是路徑的長度而不是邊的長度。

哈夫曼編碼,也是貪心思想的重要應用,它是一種最優的自由前綴變長編碼方案,基於字符在給定文本中出現頻率,把位串賦給字符,通過貪婪思想構造一顆二叉樹(哈夫曼樹),形成一組編碼方案。

 

典型題目

下面我們介紹幾道典型題目。刷了幾道題後,發現大多數貪心思想並不明顯,可以理解爲就是一些解題思路和詭計。=。=

455. Assign Cookies

Assume you are an awesome parent and want to give your children some cookies. But, you should give each child at most one cookie. Each child i has a greed factor gi, which is the minimum size of a cookie that the child will be content with; and each cookie j has a size sj. If sj >= gi, we can assign the cookie j to the child i, and the child i will be content. Your goal is to maximize the number of your content children and output the maximum number.

題目解析:

這是最典型的一道貪心的題目了。一羣孩子和一堆餅乾,我們要滿足最多的孩子。解題思路就是,對於每一個孩子來說,我們找一個能滿足他且最小的餅乾即可。

在代碼中看不到算法框架,只是一種思想。我們將孩子和餅乾排序,然後依次滿足孩子,從小餅乾選起,直到餅乾沒有合適的或者孩子都滿足爲止。

class Solution:
    def findContentChildren(self, g: List[int], s: List[int]) -> int:
        if not g or not s:
            return 0
        cookies = sorted(s)
        chs = sorted(g)
        if chs[-1] < cookies[0]:
            return len(g)
        if chs[0] > cookies[-1]:
            return 0
        j = 0
        for i, x in enumerate(chs):
            while j < len(s) and x > cookies[j]:
                j += 1
            if j > len(s)-1:
                return i
            j += 1
        return i+1

402. Remove K Digits

Given a non-negative integer num represented as a string, remove k digits from the number so that the new number is the smallest possible.

Note:

  • The length of num is less than 10002 and will be ≥ k.
  • The given num does not contain any leading zero.

Example 1:

Input: num = "1432219", k = 3
Output: "1219"
Explanation: Remove the three digits 4, 3, and 2 to form the new number 1219 which is the smallest.

題目解析:

移出幾個數字使新的數字最小,那麼我們肯定是從高位開始,儘量去掉大的數字留下小的數字,這就是其中貪心的思想。

在具體的代碼中我們是這麼做的,從前往後對於每一個數字,我們和stack中棧頂數字比較,大的話就去掉,直到k個。貪心就體現在移除數字時,我們從高位開始並且選擇當前最大的。

另外,本題還要注意一些後續處理,比如最後k>0的情況(比如數字本身就是遞增的),此時將末尾的數字依次去掉。

class Solution:
    def removeKdigits(self, num: str, k: int) -> str:
        if len(num) == k:
            return "0"
        stack = []
        for s in num:
            if k == 0:
                stack.append(s)
            else:
                n = int(s)
                while stack and int(stack[-1]) > n and k > 0:
                    stack.pop()
                    k -= 1
                stack.append(s)  
        while k > 0:
            stack.pop()
            k -= 1
        if not stack:
            stack.append("0")      
        res = "".join(stack)
        return str(int(res))

45. Jump Game II

Given an array of non-negative integers, you are initially positioned at the first index of the array.

Each element in the array represents your maximum jump length at that position.

Your goal is to reach the last index in the minimum number of jumps.

Example:

Input: [2,3,1,1,4]
Output: 2
Explanation: The minimum number of jumps to reach the last index is 2.
    Jump 1 step from index 0 to 1, then 3 steps to the last index.

Note:

You can assume that you can always reach the last index.

題目解析:

最後看一道hard 的題目,如何用最少的步驟到達最後。

這道題的難點就是貪心思想的設計,可以想到,每次都走最大的步伐,不一定是最優的。還要考慮到中間位置的最大步伐。

因此本題中貪心的思路是,在i位置,我們能到達的位置是i+nums[i],但是這不是最優的,我們要選擇一個step使得從i+step位置可以走的最遠即i+step+nums[i+step]最大,我們選擇最優的這個step_作爲當前步伐。

class Solution:
    def jump(self, nums: List[int]) -> int:
        if len(nums) <= 1:
            return 0
        i = 0
        res = 0
        while i <= len(nums)-1:
            step = nums[i]
            goal = i+step
            step_ = step
            if goal >= len(nums)-1:
                return res + 1
            
            for j in range(1, step+1):
                try_ = i + j + nums[i+j]
                if try_ > goal:
                    goal = try_
                    step_ = j
            res += 1
            i += step_
            

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章