文章目錄
一,什麼是貪婪算法
解決最簡化問題的演算法,其解題過程可看成是由一連串的決策步驟所組成,而每一步驟都有一組選擇要選定。
貪婪演算法的特性是 每一次選擇都採取區域最佳接(locally optimal solution)
,而透過每一個區域最佳解最後綜合成爲全域最佳解(globally optimal solution)
從而將問題解決
- 一個貪婪算法在每一決策步驟總是選定當下看來最好的選擇
- 貪婪算法並不保證總是得到最佳解,但在有些問題可以得到最佳解
二,最短路徑
找出下面多級圖的最短路徑
則使用貪婪算法,從S到T的最短路徑爲:1+2+5 = 8
我們再來看一個比較複雜的例子:
我們是用貪婪算法得到的最短路徑爲:(S - A - D - T)
1+4+18 = 23
而真實的最短路徑爲:(S - C - F - T)
5+2+2 = 9
可以看出又是使用貪婪算法得出的最終路徑並非是真實的最短路徑。
三,使用貪婪解題策略的演算法
- 活動選擇(activity-selection)演算法
- 揹包(Knapsack)演算法
- Huffman編碼演算法
- Kruskal最小擴張樹演算法
- Prim最小擴張樹演算法
- Dijkstra最短路徑演算法
3.1 活動選擇問題
- 假設有
n
個活動提出申請要使用一個場地,而這場地在同一時間點時最多隻能讓一個活動使用。 - 從這
n
個活動選一組數量最多,且可以在這場地舉辦的活動集。 - 假設活動 ai 其提出申請使用場地的時段爲半開半閉的區間 [ si, fi )
一個活動選擇問題
假設我們有一集合 S = { a1, a2, …,an },其中有 n 個行銷活動
- 對於每一個活動 ai,其開始時間爲 si 和結束時間爲 fi(當 0 ≤ ai < fi < ∞)
- 活動 ai在半開放時間間隔內發生 [ si, fi )
如果間隔[si,fi)和[sj,fj)不重疊,則活動ai和aj是兼容的(compatible)
- 如果si ≥ fi 或 si ≤ fi,則說 ai和aj 是兼容的(compatible)
活動選擇問題是選擇相互兼容的活動的最大子集。
我們假設活動按完成時間單調遞增的順序排序:f1 ≤ f2 ≤ f3 ≤ … ≤ fn-1 ≤ fn
解題步驟如下:
- 我們首先對這個程序制定一個
動態規劃解
,其中我們將兩個子問題的最優解結合起來,形成原始問題的最優解。 - 然後我們將觀察到,我們只需要考慮一個選擇——
貪婪的選擇
——並且當我們做出貪婪的選擇時,其中一個子問題被保證爲空,因此只剩下一個非空的子問題。
活動選擇問題的最優子結構
- Sij= { ak∈S : fi ≤ sk < fk ≤ sj }
因此,Sij 是 S 中活動的子集,可以在活動ai結束之後開始,在活動aj開始之前結束。 - 若 f0 = 0 和 sn+1 = ∞;
則 S = S0,n+1(0 ≤ i, j ≤ n+1.) - Sij = Sik ∪ {ak} ∪ Skj
遞歸解決方案
Sij = Sik ∪ {ak} ∪ Skj
c[i, j] = c[i, k] + c[k, j] + 1
活動選擇問題
設 P(A)
表示 A
爲給定的活動集的問題,S
表示 P(A)
的最優解。對於 A
中的任何活動 ai,我們有
- ai ∈! S → S 是 P(A{ai}) 的最優解
- ai ∈ S → S {ai} 是 P(A\N[i]) 的最優解,但不一定是 P(A{ai}) 的最優解
↑ 每一條橫線段代表一個活動,其橫向位置代表了發生的時段,若縱向有交叉的部分這說明兩時間衝突。
那麼,我們可以做到更好嗎?
動態規劃(Dynamic programming): O(n2)
貪婪的解決方案 - 有沒有辦法反覆做出局部決定?
- 關鍵:我們還是希望最終得到最優解
3.2 貪婪選擇(Greedy options)
1,選擇最早開始的活動,i.e. min{s1, s2, s3, …, sn}?
↓ 可以看出,選擇的不是最優解
,2,選擇時間最短的活動,i.e. min{ f1 - s1, f2 - s2, f3 - s3, …, fn - sn }?
↓ 可以看出,選擇的也不是最優解
3,選擇衝突次數最少的活動(與其共同發生的活動稱爲衝突活動)
選擇結果如下 ↓
4,選擇最早結束的活動,i.e. argmin{ f1, f2, f3, …, fn }?
藍色是我們選擇出的活動,而紅色是與藍色衝突的活動
然後把紅色活動刪掉,並且選擇出第二早結束的活動(再繼續上邊的操作:刪除衝突活動)
最終,經過選擇,會留下以下4個活動
3.3 將動態規劃解轉化爲貪婪解
考慮任何非空子問題Sij,並讓am成爲Sij中完成時間最早的活動:fm = min { fk : ak ∈ Sij}
則有如下性質:
- 活動am用於Sij中相互兼容活動的一些最大子集。
- 子問題Sim爲空,因此選擇am會使子問題Smj成爲唯一可能非空的子問題。
四,高效的貪婪算法
4.1 貪婪算法的概述及特點
一旦你確定了一個合理的貪婪算法(reasonable greedy heuristic):
- 證明它總是給出正確的答案
- 可以開發有效的解決方案
貪婪方法概述
- 貪婪法則挑選一項活動來安排
- 將該活動添加到答案中
- 刪除該活動和所有衝突的活動,稱爲 A ’
- 重複操作A ’,直到A ’爲空集合
貪婪算法的特點
- 貪婪選擇特性:通過局部最優(optimal)選擇可以得到全局最優解。
- 最優子結構:在子問題的最優解範圍內的問題的最優解。
4.2 設計貪婪算法
1,
拋出優化問題
- as one in which we make a choice
做出選擇 - and with one subproblem to solve
並且解決一個子問題
2,
證明對原來的問題總是有一個最優解,使貪婪的選擇總是安全的
3,
在做出貪婪的選擇之後
- 如果我們將子問題的最優解與我們所做的貪婪選擇相結合,我們就得到了原問題的最優解。
一個活動選擇的例子:
問題:選擇相互兼容的活動的最大子集。
我們假設活動按結束時間的單調遞增順序排序:f1 ≤ f2 ≤ f3 ≤ … ≤ fn-1 ≤ fn
爲了方便觀察可以換個簡易圖,如下:
很容易看出,相互兼容(不衝突)活動的子空間有:
- {a1; a4; a8; a11}
- {a2; a4; a9; a11}
- {a3; a9; a11}
4.3 遞歸貪婪算法
'遞歸活動選擇器(s, f, k, n)'
RECURSIVE-ACTIVITY-SELECTOR(s, f, k, n)
1 m ← k +1
2 while m ≤ n and s[m] < f [k] # Find the first activity in Sij
3 do m ← m + 1
4 if m ≤ n
5 then return {am} ∪ RAS(s, f, m, n)
6 else return 0
遞歸活動選擇器對11個活動的操作
4.4 迭代貪婪算法
'貪婪-活動-選擇器'
GREEDY-ACTIVITY-SELECTOR(s, f)
1 n ← s.length
2 A ← {a1}
3 k ← 1
4 for m ← 2 to n
5 do if s[m] ≥ f[k]
6 then A ← A ∪ {am}
7 k ← m
8 return A
五,貪婪策略的要素
- 確定問題的最優子結構;
- 開發遞歸解決方案;
- 證明如果我們做出貪婪的選擇,那麼只剩下一個子問題;
- 證明貪婪的選擇總是安全的;(步驟3和4可以按任意順序進行。)
- 開發實現貪婪策略的遞歸算法;
- 將遞歸算法轉換爲迭代算法。
另一方面,我們可以在考慮貪婪選擇的情況下,設計出最優的子結構。
5.1 貪婪與動態規劃
- 揹包問題
- 0-1揹包問題
5.1.1 揹包問題
定義
- 給定一個最大載重容量(capacity)爲 W 的揹包,以及n個可以放入揹包的物品,其中第i個物品重爲wi > 0,價格爲pi > 0。
- 目標:找出 x1,…,xn以最大化 Σ1≤i≤n pixi
- 限制條件爲: Σ1≤i≤n wixi ≤ W,其中 0≤xi≤1,1≤i≤n
對定義進行整理得出
- n個物件,揹包容量爲 W
– 每個物件,重量 wi > 0
– 每個物件,利潤 pi > 0 - 最大限度 Σ1≤i≤n pixi
- 限制條件 Σ1≤i≤n wixi ≤ W(0≤xi≤1,1≤i≤n)
5.1.2 揹包演算法
Algorithm 揹包演算法
Input
: 揹包的最大容量W,以及可以放入揹包的n個物品的非負重量wi 與 價格piOutput
: 介於0與1之間的x1,…,xn分別代表第1個,…,第n個物品放入揹包中的零碎部份。可以最大化 Σ1≤i≤n pixi,並且滿足 Σ1≤i≤n wixi ≤ W(0≤xi≤1,1≤i≤n)
1,將pi/wi由大至小排序。
2,根據此排序來將物品依序儘可能地放入揹包中,直至揹包容量W用完爲止。
揹包演算法時間複雜度
- 依 pi/wi由大至小排序: O(n log n)
- 將物品依序放入揹包: O(n)
總時間複雜度: O(n log n)
5.1.3 0-1揹包問題
定義
- 給定一個最載容量(capacity)爲W的揹包,以及n個可以放入揹包的物品,其中第i個物品的重量為 wi > 0,價格爲 pi > 0。
- 目標:目標:找出 x1,…,xn以最大化 Σ1≤i≤n pixi
- 限制條件爲: Σ1≤i≤n wixi ≤ W,其中 xi=0或1,1≤i≤n
揹包問題 與 0/1揹包問題的不同點在於:
- 在0/1揹包問題中,xi只能是0或1
- 而在揹包問題中,0 ≤ xi ≤ 1
貪婪策略不適用於0-1揹包
揹包中是否包含物品
- 我們必須將解決方案與包含物品的子問題和排除該物體的子問題的解決方案進行比較。
以這種方式表述的問題會產生許多重疊的子問題——動態規劃的一個特徵
5.2 Huffman編碼
字元編碼(character coding)可以分爲:
- 固定長度編碼: 如ACSII、Unicode
- 可變長度編碼: Huffman code
Huffman編碼以字首碼(prefix code)方式達到字元編碼最佳資料壓縮(optimal data compression)
- 字首碼 (prefix code): 任何字元編碼一定不是其他字元編碼的字首(prefix)。
- 可以使用二元樹來呈現,達到簡單編碼(encoding)與解碼(decoding)的功能。
5.2.1 Huffman編碼範例
假設給定一個僅用到a, b, c, d, e五個字元的文件,現在想針對五個字元進行編碼,以下是可能的固定長度編碼與可變長度的Huffman字首碼。
字首碼(Prefix code) 讓出現頻率較高字元的編碼較短, 以達到使用最少位元就可以將所有資料儲存的目標。
100000個字符的數據文件只包含字符a–f
- 爲每個字符分配一個3位碼字,文件編碼爲300,000位
- 使用所示的可變長度代碼,我們可以將文件編碼爲224,000位(節省大約25%)
5.2.2 樹對應於編碼方案
5.2.3 構造哈夫曼碼
HUFFMAN( C )
1 n ← |C|
2 Q ← C
3 for i ← 1 to n – 1
4 do allocate a new node z
5 left[z] ← x ← EXTRACT-MIN(Q)
6 right[z] ← y ← EXTRACT-MIN(Q)
7 f[z] ← f[x] + f[y]
8 INSERT(Q, z)
9 return EXTRACT-MIN(Q)
Huffman編碼演算法時間複雜度
- 行2: O(n)建立優先佇列Q
- 行3-8: for迴圈一共執行n-1次,而且迴圈中的優先佇列操作均爲O(log n)複雜度,因此整個迴圈具有O(n log n)的複雜度
總時間複雜度: O(n log n)
5.2.4 Huffman演算法的步驟:
六,使用貪婪解題策略的演算法
- 活動選擇(activity-selection)演算法
- 揹包(Knapsack)演算法
- Huffman編碼演算法
- Kruskal最小擴張樹演算法
- Prim最小擴張樹演算法
- Dijkstra最短路徑演算法