1. 程式人生 > >演算法(六):圖解貪婪演算法

演算法(六):圖解貪婪演算法

演算法簡介

貪婪演算法(貪心演算法)是指在對問題進行求解時,在每一步選擇中都採取最好或者最優(即最有利)的選擇,從而希望能夠導致結果是最好或者最優的演算法。

貪婪演算法所得到的結果往往不是最優的結果(有時候會是最優解),但是都是相對近似(接近)最優解的結果。

  • 貪婪演算法並沒有固定的演算法解決框架,演算法的關鍵是貪婪策略的選擇,根據不同的問題選擇不同的策略。

  • 必須注意的是策略的選擇必須具備無後效性,即某個狀態的選擇不會影響到之前的狀態,只與當前狀態有關,所以對採用的貪婪的策略一定要仔細分析其是否滿足無後效性。

比如前邊介紹的最短路徑問題(廣度優先狄克斯特拉)都屬於貪婪演算法,只是在其問題策略的選擇上,剛好可以得到最優解。

基本思路

其基本的解題思路為:

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]
複製程式碼

公眾號

有興趣可以關注微信公眾號,共同進步