演算法簡介
參考:www.cnblogs.com/steven_oyj/…
貪婪演算法(貪心演算法)是指在對問題進行求解時,在每一步選擇中都採取最好或者最優(即最有利)的選擇,從而希望能夠導致結果是最好或者最優的演算法。
貪婪演算法所得到的結果往往不是最優的結果(有時候會是最優解),但是都是相對近似(接近)最優解的結果。
-
貪婪演算法並沒有固定的演算法解決框架,演算法的關鍵是貪婪策略的選擇,根據不同的問題選擇不同的策略。
-
必須注意的是策略的選擇必須具備無後效性,即某個狀態的選擇不會影響到之前的狀態,只與當前狀態有關,所以對採用的貪婪的策略一定要仔細分析其是否滿足無後效性。
比如前邊介紹的最短路徑問題(廣度優先、狄克斯特拉)都屬於貪婪演算法,只是在其問題策略的選擇上,剛好可以得到最優解。
基本思路
其基本的解題思路為:
1.建立數學模型來描述問題
2.把求解的問題分成若干個子問題
3.對每一子問題求解,得到子問題的區域性最優解
4.把子問題對應的區域性最優解合成原來整個問題的一個近似最優解
案例
這邊的案例來自"演算法圖解"一書
案例一
區間排程問題:
假設有如下課程,希望儘可能多的將課程安排在一間教室裡:
課程 | 開始時間 | 結束時間 |
---|---|---|
美術 | 9AM | 10AM |
英語 | 9:30AM | 10:30AM |
數學 | 10AM | 11AM |
計算機 | 10:30AM | 11:30AM |
音樂 | 11AM | 12PM |
這個問題看似要思考很多,實際上演算法很簡單:
1.選擇結束最早的課,便是要在這教室上課的第一節課 2.接下來,選擇第一堂課結束後才開始的課,並且結束最早的課,這將是第二節在教室上的課。
重複這樣做就能找出答案,這邊的選擇策略便是結束最早且和上一節課不衝突的課進行排序,因為每次都選擇結束最早的,所以留給後面的時間也就越多,自然就能排下越多的課了。
每一節課的選擇都是策略內的區域性最優解(留給後面的時間最多),所以最終的結果也是近似最優解(這個案例上就是最優解)。 (該案例的程式碼實現,就是一個簡單的時間遍歷比較過程)
案例二
揹包問題:有一個揹包,容量為35磅 , 現有如下物品
物品 | 重量 | 價格 |
---|---|---|
吉他 | 15 | 1500 |
音響 | 30 | 3000 |
膝上型電腦 | 20 | 2000 |
顯示器 | 29 | 2999 |
筆 | 1 | 200 |
要求達到的目標為裝入的揹包的總價值最大,並且重量不超出。
方便計算所以只有3個物品,實際情況可能是成千上萬。
同上使用貪婪演算法,因為要總價值最大,所以每次每次都裝入最貴的,然後在裝入下一個最貴的,選擇結果如下:
選擇: 音響 + 筆,總價值 3000 + 200 = 3200
並不是最優解: 吉他 + 膝上型電腦, 總價值 1500 + 2000 = 3500
當然選擇策略有時候並不是很固定,可能是如下:
(1)每次挑選價值最大的,並且最終重量不超出:
選擇: 音響 + 筆,總價值 3000 + 200 = 3200
(2)每次挑選重量最大的,並且最終重量不超出(可能如果要求裝入最大的重量才會優先考慮):
選擇: 音響 + 筆,總價值 3000 + 200 = 3200
(3)每次挑選單位價值最大的(價格/重量),並且最終重量不超出:
選擇: 筆+ 顯示器,總價值 200 + 2999 = 3199
如上最終的結果並不是最優解,在這個案例中貪婪演算法並無法得出最優解,只能得到近似最優解,也算是該演算法的侷限性之一。該類問題中需要得到最優解的話可以採取動態規劃演算法(後續更新,也可以關注我的公眾號第一時間獲取更新資訊)。
案例三
集合覆蓋問題:
假設存在如下表的需要付費的廣播臺,以及廣播臺訊號可以覆蓋的地區。 如何選擇最少的廣播臺,讓所有的地區都可以接收到訊號。
廣播臺 | 覆蓋地區 |
---|---|
K1 | ID,NV,UT |
K2 | WA,ID,MT |
K3 | OR,NV,CA |
K4 | NV,UT |
K5 | CA,AZ |
... | ... |
如何找出覆蓋所有地區的廣播臺的集合呢,聽起來容易,實現起來很複雜,使用窮舉法實現:
(1) 列出每個可能的廣播臺的集合,這被稱為冪集。假設總的有n個廣播臺,則廣播臺的組合總共有2ⁿ個
(2) 在這些集合中,選出覆蓋全部地區的最小的集合,假設n不在,但是當n非常大的時候,假設每秒可以計算10個子集
廣播臺數量n | 子集總數2ⁿ | 需要的時間 |
---|---|---|
5 | 32 | 3.2秒 |
10 | 1024 | 102.4秒 |
32 | 4294967296 | 13.6年 |
100 | 1.26*100³º | 4x10²³年 |
目前並沒有演算法可以快速計算得到準備的值, 而使用貪婪演算法,則可以得到非常接近的解,並且效率高:
選擇策略上,因為需要覆蓋全部地區的最小集合:
(1) 選出一個廣播臺,即它覆蓋了最多未覆蓋的地區即便包含一些已覆蓋的地區也沒關係 (2) 重複第一步直到覆蓋了全部的地區
這是一種近似演算法(approximation algorithm,貪婪演算法的一種)。在獲取到精確的最優解需要的時間太長時,便可以使用近似演算法,判斷近似演算法的優劣標準如下:
- 速度有多快
- 得到的近似解與最優解的接近程度
在本例中貪婪演算法是個不錯的選擇,不僅執行速度快,本例執行時間O(n²),最壞的情況,假設n個廣播臺,每個廣播臺就覆蓋1個地區,n個地區,總計需要查詢n*n=O(n²),實現可檢視後面的java程式碼實現
廣播臺數量n | 子集總數2ⁿ | 窮舉需要時間 | 貪婪演算法 |
---|---|---|---|
5 | 32 | 3.2秒 | 2.5秒 |
10 | 32 | 102.4秒 | 10秒 |
32 | 32 | 13.6年 | 102.4秒 |
100 | 32 | 4x10²³年 | 1000秒 |
此時演算法選出的是K1, K2, K3, K5,符合覆蓋了全部的地區,可能不是預期中的K2, K3,K4,K5(也許預期中的更便宜,更便於實施等等)
NP完全問題
案例四:
旅行商問題
假設有旅行商需要從下面三個城市的某一個城市出發,如何規劃路線獲取行程的最短路徑。
存在3!(階乘)=6種可能情況:
A->B->C
A->C->B
B->A->C
B->C->A
C->A->B
C->B->A
複製程式碼
這邊和之前求最短路徑的演算法(廣度搜尋、狄克斯特拉、貝爾曼-福特),最大的差別是沒有固定源點(起點),,每一個節點都可能是源點,並且需要經過每一個節點,所以若窮舉法則不得不找出每一種可能並進行比較。
當城市數量為n,則可能性為n!,假設每秒處理判斷一個路線
數量n | 總數n! | 窮舉需要時間 |
---|---|---|
5 | 120 | 120秒 |
10 | 32 | 42天 |
而使用貪婪演算法,隨機選擇從一個城市出發,比如A,每次選擇從最近的還沒去過的城市出發,則可以得到近似最優解。
第一次比較n-1個城市 第二次比較n-2個城市 ... 第n-1次比較1個城市 第n次不存在需要比較的了個
0+1+2+3+..+(n-1) ≈ O(n²/2)
數量n | 總數n! | 窮舉需要時間 | 貪婪需要時間 |
---|---|---|---|
5 | 120 | 120秒 | 12.5秒 |
10 | 32 | 42天 | 50秒 |
類似上述集合覆蓋問題、旅行商問題,都屬於NP完全問題,在數學領域上並沒有快速得到最優解的方案,貪婪演算法是最適合處理這類問題的了。
如何判斷是NP完全問題的:
1.元素較少時,一般執行速度很快,但隨著元素數量增多,速度會變得非常慢 2.涉及到需要計算比較"所有的組合"情況的通常是NP完全問題 3.無法分割成小問題,必須考慮各種可能的情況。這可能是NP完全問題 4.如果問題涉及序列(如旅行商問題中的城市序列)且難以解決,它可能就是NP完全問題 5.如果問題涉及集合(如廣播臺集合)且難以解決,它可能就是NP完全問題 6.如果問題可轉換為集合覆蓋問題或旅行商問題,那它肯定是NP完全問題
小結
1.貪婪演算法可以尋找區域性最優解,並嘗試與這種方式獲得全域性最優解
2.得到的可能是近似最優解,但也可能便是最優解(區間排程問題,最短路徑問題(廣度優先、狄克斯特拉))
3.對於完全NP問題,目前並沒有快速得到最優解的解決方案
4.面臨NP完全問題,最佳的做法就是使用近似演算法
5.貪婪演算法(近似演算法)在大部分情況下易於實現,並且效率不錯
java實現
貪婪演算法需要根據具體問題,選擇對應的策略來實現,所以這邊只取集合覆蓋問題做個示例:
/**
* 貪婪演算法 - 集合覆蓋問題
* @author Administrator
*
*/
public class Greedy {
public static void main(String[] args){
//初始化廣播臺資訊
HashMap<String,HashSet<String>> broadcasts = new HashMap<String,HashSet<String>>();
broadcasts.put("K1", new HashSet(Arrays.asList(new String[] {"ID","NV","UT"})));
broadcasts.put("K2", new HashSet(Arrays.asList(new String[] {"WA","ID","MT"})));
broadcasts.put("K3", new HashSet(Arrays.asList(new String[] {"OR","NV","CA"})));
broadcasts.put("K4", new HashSet(Arrays.asList(new String[] {"NV","UT"})));
broadcasts.put("K5", new HashSet(Arrays.asList(new String[] {"CA","AZ"})));
//需要覆蓋的全部地區
HashSet<String> allAreas = new HashSet(Arrays.asList(new String[] {"ID","NV","UT","WA","MT","OR","CA","AZ"}));
//所選擇的廣播臺列表
List<String> selects = new ArrayList<String>();
HashSet<String> tempSet = new HashSet<String>();
String maxKey = null;
while(allAreas.size()!=0) {
maxKey = null;
for(String key : broadcasts.keySet()) {
tempSet.clear();
HashSet<String> areas = broadcasts.get(key);
tempSet.addAll(areas);
//求出2個集合的交集,此時tempSet會被賦值為交集的內容,所以使用臨時變數
tempSet.retainAll(allAreas);
//如果該集合包含的地區數量比原本的集合多
if (tempSet.size()>0 && (maxKey == null || tempSet.size() > broadcasts.get(maxKey).size())) {
maxKey = key;
}
}
if (maxKey != null) {
selects.add(maxKey);
allAreas.removeAll(broadcasts.get(maxKey));
}
}
System.out.print("selects:" + selects);
}
}
複製程式碼
執行完main方法列印資訊如下:
selects:[K1, K2, K3, K5]
複製程式碼
公眾號
有興趣可以關注微信公眾號,共同進步