這幾道經典例題幫你輕鬆搞透貪心算法

貪心算法概念敘述

運用貪心算法求解問題時,會將問題分爲若干個子問題,可以將其想象成俄羅斯套娃,利用貪心的原則從內向外依次求出當前子問題的最優解,也就是該算法不會直接從整體考慮問題,而是想要達到局部最優。只有內部的子問題求得最優解,才能繼續解決包含該子問題的下一個子問題,所以前一個子問題的最優解會是下一個子問題最優解的一部分,重複這個操作直到堆疊出該問題的最優解。

貪心算法最關鍵的部分在於貪心策略的選擇,貪心選擇的意思是對於所求問題的整體最優解可以通過一系列的局部最優選擇求得。而必須注意的是,貪心選擇必須具備無後效性,也就是某個狀態不會影響之前求得的局部最優解。

運動貪心算法解決相應問題時會比較簡單和高效,省去了尋找全局最優解很多不必要的窮舉操作,由於貪心算法問題並沒有固定的貪心策略,所以唯一的難點就是找到帶求解問題的貪心策略,但畢竟熟能生巧嘛,算法的基本思想總是固定不變的。

貪心算法求解步驟

  1. 將問題分解爲若干個子問題
  2. 找出適合的貪心策略
  3. 求解每一個子問題的最優解
  4. 將局部最優解堆疊成全局最優解

下面通過利用貪心算法解決四道LeetCode題目,加深一下對貪心算法思想的掌握,其中第一道爲easy,其餘三道爲medium,會標註出相應的題號。

來源:力扣(LeetCode)
鏈接:https://leetcode-cn.com

455.分發餅乾

問題描述:假設你是一位很棒的家長,想要給你的孩子們一些小餅乾。但是,每個孩子最多隻能給一塊餅乾。對每個孩子 i ,都有一個胃口值 gi ,這是能讓孩子們滿足胃口的餅乾的最小尺寸;並且每塊餅乾 j ,都有一個尺寸 sj 。如果 sj >= gi ,我們可以將這個餅乾 j 分配給孩子 i ,這個孩子會得到滿足。你的目標是儘可能滿足越多數量的孩子,並輸出這個最大數值。

注意:

  • 你可以假設胃口值爲正。
  • 一個小朋友最多隻能擁有一塊餅乾

這道題的思路主要包括兩個點:

  1. 儘量先滿足胃口值小的孩子,因爲這樣的孩子容易滿足。
  2. 進行條件1時,儘可能選用尺寸小的,這樣大尺寸餅乾可以用來滿足胃口值大的孩子。

這道題的貪心思想非常明顯,就是要儘可能地滿足更多的孩子,而胃口值小的孩子是容易滿足的,反之胃口值大的孩子很難滿足,所以在抉擇上儘可能滿足前者、餓着後者。

這個思想可以類比於賽馬,我們假設贏或者平作爲滿足條件。如果A的3贏了B的1,那麼剩下兩匹的結果可能就是一平一負或者兩負,那麼此時至多才是1滿足;而如果A的馬和B的馬都按照順序比,則可以達到3平,那麼此時可以達到3滿足。

所以綜上可以得到解題思路,首先需要將胃口值和餅乾尺寸由小至大排序。設定一個計數器child,用來記得到滿足的孩子個數,再維護一個餅乾指針cookies。如果餅乾尺寸可以滿足孩子胃口值,即g[child]<=s[cookies],就將child、cookies分別加一(向後移動一位),否則只將cookies向後移動一位。因爲孩子的胃口值是由小到大的,若不滿足當前的胃口值更不會滿足之後的。

時間複雜度:兩次排序加一次循環,如果選擇時間複雜度較優的排序方法,那麼O(n)=O(nlogn)+O(nlogn)+O(n)=O(nlogn)O(n)=O(nlogn)+O(nlogn)+O(n)=O(nlogn)

55. 跳躍遊戲

題目描述:給定一個非負整數數組,你最初位於數組的第一個位置。數組中的每個元素代表你在該位置可以跳躍的最大長度。判斷你是否能夠到達最後一個位置。

在解題之前首先明確一下解題目標,若要滿足能夠到達最後一個位置,那麼就需要最後一跳的最大距離加上該位置下標一定要大於等於數組長度,即nums[i]+i>=length(nums),而當前元素又一定處於之前元素最遠可以達到範圍之內,這樣層層嵌套不就是貪心算法思想中的子問題的形式嘛。

我們要從數組的第一個元素開始遍歷,並且維護一個最遠可以到達的位置(max_i),當遍歷到數組中的某一個位置i時,如果i在max_i範圍之內,並且此時最遠可以達到位置大於max_i,那麼就通過i+nums[i]更新max_i,如果在遍歷過程中max_i大於等於數組長度,則代表可以達到最後一個位置,反之不能。要注意的是,max_i既不是數組下標也不是數組中某個元素,而是二者的加和。

拿上面兩個示例爲例:

  • 示例1:最開始下標爲0的元素值爲2,此時max_i=2,所以下標1、2都在max_i之內,當達到下標1時,此時max_i = 1+3 = 4,所以可以達到最後一個位置。
  • 示例2:最開始下標爲0的元素值爲3,此時max_i=3,下標1、2、3在範圍內,但在遍歷這三個位置時會發現max_i=2+1=1+2=0+3總是等於3,而3<4,所以最後一個位置永遠達不到。


時間複雜度:O(n)O(n),一層循環。

435.無重疊區間

題目描述:給定一個區間的集合,找到需要移除區間的最小數量,使剩餘區間互不重疊。

注意:

  1. 可以認爲區間的終點總是大於它的起點。
  2. 區間 [1,2] 和 [2,3] 的邊界相互“接觸”,但沒有相互重疊。


本題的要求是“找到需要移出區間的最小數量”,換句話說就是要更多地保留集合中的區間,那麼對於有重疊的區間,就應該儘可能刪去跨度較大的區間。

這裏我們根據區間的終點進行貪心選擇,不是說起點不行,而是終點更好,那原因呢?因爲如果每次選擇的區間結尾越小,留給後面區間的空間自然就變多了,那麼後面能留下的區間數量也就越多。用一句話概括就是每次都選擇終點最小的,因爲這一定是最優解的一部分,這不就是正是貪心思想的應用嘛。

解這道題時需要先將數組按照區間的終點進行排序,然後需要維護一個end指針,它代表當前集合中的最小終點,在遍歷數組時,若當前元素的起點大於前一區間的終點,那麼不重疊區間的計數器加一,更新end指針;反之則不做任何操作,最後區間總數減去不重疊區間即爲需要移除區間的最小數量。

時間複雜度:一次排序加一次循環,O(n)=O(nlogn)+O(n)=O(nlogn)O(n)=O(nlogn)+O(n)=O(nlogn)

LeetCode中第452題:用最少數量的箭引爆氣球與本題解法十分相似,大家可以類比本題的思路自己練習。

376. 擺動序列

如果連續數字之間的差嚴格地在正數和負數之間交替,則數字序列稱爲擺動序列。第一個差(如果存在的話)可能是正數或負數。少於兩個元素的序列也是擺動序列。

例如, [1,7,4,9,2,5] 是一個擺動序列,因爲差值 (6,-3,5,-7,3) 是正負交替出現的。相反, [1,4,7,2,5] 和 [1,7,4,5,5] 不是擺動序列,第一個序列是因爲它的前兩個差值都是正數,第二個序列是因爲它的最後一個差值爲零。

給定一個整數序列,返回作爲擺動序列的最長子序列的長度。 通過從原始序列中刪除一些(也可以不刪除)元素來獲得子序列,剩下的元素保持其原始順序。

示例1和示例3比較特殊,一個是完全擺動序列,另一個是完全升序序列,所以這裏利用比較普通的示例2講解,依據示例2中的數組可以大致繪製出一個元素分佈圖,如下:

其中橙色點就構成了一個擺動序列,所以橙色點的個數也是最終要輸出的結果。可以看到[5,10,13,15]是一個連續遞增的子序列,5處於17之後是符合題意的,所以一定將其保留,而對於[10,13,15]三個元素,只有保留15纔可以形成擺動序列。

所以對於一段連續遞增的子序列,只有保留這段子序列的首尾元素時,才能形成一個擺動序列,並且這也加大了尾部的後一個元素成爲擺動序列的下一個元素的可能性。同理連續遞減的子序列也做如上操作,比如圖中的[15,10,5]。

解決這道題的關鍵就在於如何保留連續連續遞增的子序列首尾元素,結合棧是一個很好的方法,但出棧入棧的條件是什麼呢?我們維護一個狀態值nowstate,他共有"up"和"down"兩種取值,"up"表示該元素大於前一個元素,"dowm"表示該元素小於前一個元素。

從第二個元素開始遍歷數組,因爲第一個元素(下標爲0)一定處於擺動序列內嘛,判斷如果當前元素大於前一個元素且nowstate=“up”,這就說明連續遞增出現了,就需要從棧移除前一個元素。如果不是就更新nowstate爲"up",因爲此時前一個nowstate=“down”,另一種可能性同理。不論什麼條件下都要做入棧操作,因爲這裏只靠條件過濾不符合的元素。

時間複雜度:O(n)O(n),一層循環。

總結

從上面幾道題中不難看出只要依據題意找出相應的貪心策略,解題就十分容易,並且代碼也不復雜,但貪心選擇的方法並不唯一,主要還是靠對算法的理解和解題的經驗。貪心算法和動態規劃是原理有些相似的兩種算法,同一問題利用不同算法解題的思路、難易程度各不相同,不要相互混淆。

關注公衆號【奶糖貓】第一時間獲取更多精彩好文

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