最近開始學習王爭老師的《數據結構與算法之美》,通過總結再加上自己的思考的形式記錄這門課程,文章主要作爲學習歷程的記錄。
首先來看一個例子,假設我們有一個可以容納100kg物品的揹包
爲了使揹包中所裝物體的總價值最大,如何選擇在揹包中裝哪些豆子?這個問題的解決是將單價從高到低排列,它的本質藉助的是貪心算法。
總結一下貪心算法解決問題:
第一步,當我們看到這類問題時,首先要聯想到貪心算法。針對一組數據,我們定義了限制值和期望值,希望從中選出幾個數據,在滿足限制值的情況下,期望值最大。
第二步,我們嘗試看下這個問題是否可以用貪心算法解決。每次選擇當前情況下,在對限制值同等貢獻量的情況下,對期望值貢獻最大的數據。
第三步,我們舉幾個例子看下貪心算法產生的結果是否是最優的。大部分情況下,舉幾個例子驗證一下就可以了。
實際上,用貪心算法解決問題的思路,並不總能給出最優解。舉個例子,在一個有權圖中,從頂點S開始,找一條到頂點T的最短路徑(路徑中邊的權值和最小)。貪心算法的解決思路是,每次都選擇一條跟當前頂點相連的權最小的邊,直到找到頂點T。
按照這個思路,我們求出的最短路徑是S-A-E-T,路徑長度是1+4+4=9.
但這種貪心的選擇方式,最終求得路徑並不是最短路徑,因爲路徑S-B-D-T纔是最短路徑,因爲這條路徑的長度是2+2+2=6.在這個問題上,貪心算法不工作的主要原因是前面的選擇會影響後面的選擇。即便第一步選擇最優的走法,但有可能因爲這一步選擇,導致後面選擇都很糟糕。
貪心算法實戰分析
1、分糖果
我們有m個糖果和n個孩子,m<n。要將糖果分給孩子喫,每個糖果的大小不等,這m個糖果的大小分別是S1、S2、S3、…、Sm。除此之外,每個孩子對糖果大小的需求不一樣,只有糖果的大小大於等於孩子的對糖果大小的需求時,孩子纔得到滿足假設這n個孩子對糖果大小需求爲g1、g2、g3、…、gn,如何分配糖果才能滿足最多數量的孩子?
這個問題可以抽象成從n個孩子中,抽取一部分孩子分配糖果,讓滿足的孩子的個數最大。這個問題的限制值就是糖果個數m。
採用貪心算法,我們可以從需求小的孩子開始分配糖果。因爲滿足一個需求大的孩子跟滿足一個需求小的孩子,對期望值的貢獻是一樣的。我們每次從剩下的孩子中,找出對糖果大小需求最小的,然後發給他剩下的糖果中能滿足他的最小的糖果,這樣得到的分配方案,也就是滿足的孩子個數最多的方案。
以力扣455題爲例,假設你是一位很棒的家長,想要給你的孩子們一些小餅乾。但是,每個孩子最多隻能給一塊餅乾。對每個孩子 i ,都有一個胃口值 gi ,這是能讓孩子們滿足胃口的餅乾的最小尺寸;並且每塊餅乾 j ,都有一個尺寸 sj 。如果 sj >= gi ,我們可以將這個餅乾 j 分配給孩子 i ,這個孩子會得到滿足。你的目標是儘可能滿足越多數量的孩子,並輸出這個最大數值。(你可以假設胃口值爲正。一個小朋友最多隻能擁有一塊餅乾。)
class Solution(object):
def findContentChildren(self, g, s):
num = 0
g = sorted(g)
s = sorted(s)
for ele in s:
if num<len(g) and ele>=g[num]:
num+=1
return num
2、錢幣找零
假設我們有1元、2元、5元、10元、20元、50元、100元這些面額的紙幣,它們的張數分別是C1、C2、C5、C10、C20、C50、C100.現在要用這些錢來支付K元,最少要用多少張紙幣?
在貢獻相同期望值(紙幣數目)的情況下,我們希望多貢獻點金額。這樣可以讓紙幣數更少。
3、區間覆蓋
假設我們有 n 個區間,區間的起始端點和結束端點分別是 [l1, r1],[l2, r2],[l3, r3],……,[ln, rn]。我們從這 n 個區間中選出一部分區間,這部分區間滿足兩兩不相交(端點相交的情況不算相交),最多能選出多少個區間呢?
這個問題的解決思路:假設這 n 個區間中最左端點是 lmin,最右端點是 rmax。這個問題就相當於,我們選擇幾個不相交的區間,從左到右將 [lmin, rmax] 覆蓋上。我們按照起始端點從小到大的順序對這 n 個區間排序。我們每次選擇的時候,左端點跟前面的已經覆蓋的區間不重合的,右端點又儘量小的,這樣可以讓剩下的未覆蓋區間儘可能的大,就可以放置更多的區間。
用貪心算法實現霍夫曼編碼
假設有1個包含1000個字符的文件,每個字符佔1個byte(1個byte=8bit),存儲這1000個字符就一共需要8000bits,需要一種更節省空間的存儲方式。
假設通過統計這1000個字符只包含6個不同的字符,分別爲a、b、c、d、e、f。若用3個二進制位(bit)就可以表示8個不同字符,此時用3000個bits即可。
這時候霍夫曼編碼出現了。霍夫曼編碼可以更加節省空間。它不僅會考察文本中有多少個字符,還會考察每個字符出現的頻率。根據頻率的不同,選擇不同長度的編碼。
這時候,1000個字符只需要2100bits。
處理過程:我們把每個字符看作一個節點,並且輔帶着把頻率放到優先級隊列中。我們從隊列中取出頻率最小的兩個節點 A、B,然後新建一個節點 C,把頻率設置爲兩個節點的頻率之和,並把這個新節點 C 作爲節點 A、B 的父節點。最後再把 C 節點放入到優先級隊列中。重複這個過程,直到隊列中沒有數據。
我們給每一條邊加上畫一個權值,指向左子節點的邊我們統統標記爲 0,指向右子節點的邊,我們統統標記爲 1,那從根節點到葉節點的路徑就是葉節點對應字符的霍夫曼編碼。
節點的路徑就是葉節點對應字符的霍夫曼編碼。
參考資料:王爭《數據結構與算法之美》