1. 程式人生 > >演算法(七):圖解動態規劃

演算法(七):圖解動態規劃

演算法簡介

動態規劃,將大問題劃分為小問題進行解決,從而一步步獲取最優解的處理演算法

與貪婪演算法區別

  • 2者都是將大問題劃分為規模更小的子問題

  • 動態規劃實質是分治法以及解決冗餘,將各個子問題的解儲存下來,讓後面再次遇到的時候可以直接引用,避免重複計算,動態規劃的顯著特徵之一,會有大量的子問題重複,可以直接使用前面的解

  • 貪心演算法的每一次操作都對結果產生直接影響(處理問題的範圍越來越小),而動態規劃則不是。貪心演算法對每個子問題的解決方案都做出選擇,不能回退;動態規劃則會根據以前的選擇結果對當前進行選擇,有回退功能(比如揹包問題,同一列相同容量的小揹包越往後才是最優解,推翻前邊的選擇)。動態規劃主要運用於二維或三維問題,而貪心一般是一維問題

  • 貪婪演算法結果是最優近似解,而動態規劃是最優解

  • 動態規劃類似搜尋或者填表的方式來,具有最優子結構的問題可以採用動態規劃,否則使用貪婪演算法

案例

這邊的案例來自"演算法圖解"一書

案例一

揹包問題:有一個揹包,容量為4磅 , 現有如下物品

物品 重量 價格
吉他(G) 1 1500
音響(S) 4 3000
電腦(L) 3 2000

要求達到的目標為裝入的揹包的總價值最大,並且重量不超出。

類似問題在前邊""貪婪演算法"一文介紹了求出近似解,現在使用動態規劃求出最優解。

解決類似的問題可以分解成一個個的小問題進行解決,假設存在揹包容量大小分為1,2,3,4的各種容量的揹包(分配容量的規則為最小重量的整數倍):

例如:

物品 1磅 2磅 3磅 4磅
吉他(G)
音響(S)
電腦(L)

對於第一行(i=1), 目前只有吉他可以選擇,所以

物品 1磅 2磅 3磅 4磅
吉他(G) 1500(G) 1500(G) 1500(G) 1500(G)
音響(S)
電腦(L)

對於第二行(i=2),目前存在吉他和音響可以選擇,所以

物品 1磅 2磅 3磅 4磅
吉他(G) 1500(G) 1500(G) 1500(G) 1500(G)
音響(S) 1500(G) 1500(G) 1500(G) 3000(S)
電腦(L)

對於第三行(i=3),目前存在吉他和音響、電腦可以選擇,所以

物品 1磅 2磅 3磅 4磅
吉他(G) 1500(G) 1500(G) 1500(G) 1500(G)
音響(S) 1500(G) 1500(G) 1500(G) 3000(S)
電腦(L) 1500(G) 1500(G) 2000(L) 3500(L+G)

以上的都符合公式:

F(i,j) = max{ F(i-1,j), W(i) + F(i,j = j-V(i))} 
即max{上面一個單元格的值, 當前商品的價值 + 剩餘空間的最大價值}

F(i,j)的代表 i行j列可以獲得的最大價值,W(i)代表該行物品的價值,V(i)在此處代表該行物品所佔據的空間重量,
F(i,j = j-V(i)) : 假設
複製程式碼

比如F(3,4) = max{ F(2,4), F(3,3) + 2000 } = max { 3000, 1500 + 2000} = 3500, 該問題的時間複雜度O(V*N),V位揹包容量,N為物品總數,即表格格子總數。

上述揹包空間的劃分依據一般根據最小的物品所佔的大小整數倍進行劃分(這邊是吉他,佔據1磅),假設多了個0.5磅的東西則,需要劃分為更細的粒度(0.5,1,1.5,2,2.5,3,3.5,4)

並且會發飆沿著一列往下走時,最大價值不會降低,因為每次計算迭代時,都選取的最大值。並且結果與行的順序並無關係,比如更換為:

使用上述公式計算,當揹包為4磅,可裝入的最大價值依舊為 3500:

物品 1磅 2磅 3磅 4磅
音響(S) 3000(S)
電腦(L) 2000(L) 3000(S)
吉他(G) 1500(G) 1500(G) 2000(G) 3500(L+G)

計算過程:

i=1: (1,1)、(1,2)、(1,3) : 因為 j=1,2,3時 < (V(i) = 4) 所以裝不下,置空 (1,4) : max{ F(i-1,j), W(i) + F(i,j-V(i))} = max{ F(0,4),3000 + F(1,0)} = 3000

i=2: (2,1)、(2,2) : 因為 j=1,2時 < (V(i) = 4、V(i-1)=3) 所以裝不下,置空 (2,3): max{ F(1,3), W(2) + F(2,0)} = 2000 (2,4): max{ F(1,4), W(2) + F(2,1)} = max{ 3000, 2000 + 0} = 3000

i=3: (3,1) : max{ F(2,1), 1500 + F(3,0)} = 1500 (3,2) : max{ F(2,2), 1500 + F(3,1)} = 1500 (因為吉他只有一把,無法重複放入) (3,3) : max{ F(2,3), 1500 + F(3,2)} = max{2000,1500} = 2000 (因為吉他只有一把,無法重複放入) (3,4) : max{ F(2,4), 1500 + F(3,3)} = max{3000,3500} = 3500

案例二

旅遊行程最優化:

假設有2天的時間,想要去如下地方旅遊,如何好好利用,使得總評分高:

名勝 時間 評分
倫敦教堂(A) 0.5天 7
倫敦劇場(B) 0.5天 6
倫敦美術館(C) 1天 9
倫敦博物館(D) 2天 9
倫敦運動館(E) 0.5天 8

該問題其實也是一個揹包,只是容量變成了時間,處理方式同上,很快便可以得出:

F(i,j) = max{ F(i-1,j), W(i) + F(i,j-V(i))} 即max{上面一個單元格的值, 當前商品的價值 + 剩餘空間的價值}

F(i,j)的代表 i行j列可以獲得的最大價值,W(i)代表該行物品的價值,V(i)在此處代表該行物品所佔據的空間重量
複製程式碼
名勝 0.5天 1天 1.5天 2天
倫敦教堂(A) 7(A) 7(A) 7(A) 7(A)
倫敦劇場(B) 7(A) 13(A+B) 13(A+B) 13(A+B)
倫敦美術館(C) 7(A) 13(A+B) 16(A+C) 22(A+B+C)
倫敦博物館(D) 7(A) 13(A+B) 16(A+C) 22(A+B+C)
倫敦運動館(E) 8(E) 15(A+E) 21(A+B+E) 24(A+C+E)

侷限性

動態規劃的侷限性之一便是每次處理時,考慮的是整件物品進行處理,無法支援拿走幾分之幾的做法,比如案例一修改為:

    揹包4磅,1.有一整袋10磅的燕麥,每磅6美元 ; 
                 2.有一整袋10磅的大米,每磅3美元 ; 
                 3.有一整袋10磅的土豆,每磅5美元 ; 
        
        因為整袋無法裝入,情況不再是要麼拿要麼不拿,而是開啟包裝拿物品的一部分,這種情況下動態規劃就無法處理。動態規劃只適合於整件物品處理的情況。但使用前面介紹的貪婪演算法則很合適,一個勁拿最貴的,拿光後再拿下一個最貴。
複製程式碼

動態規劃的侷限性之二便是無法處理相互依賴的情況,比如案例二中,增加想要去的3個地點

名勝 時間 評分
巴黎鐵塔(F) 1.5天 8
巴黎大學(G) 1.5天 9
巴黎醫院(H) 1.5天 7
從這些地方還需要很長時間,因為得從倫敦前往巴黎,這需要0.5天時間(1.5天包含了0.5天的路程消耗)。如果這3個地方都去的話,是總的需要1.5 * 3= 4.5天? 其實並不是,到達巴黎後,連續玩這3個地方其實只需 1.5 + 1 + 1 = 3.5天。 這種將 "巴黎鐵塔"裝入"揹包"會使得"巴黎大學"、"巴黎醫院"變便宜的情況,無法使用動態規劃來進行建模。
複製程式碼

動態規劃功能雖然強大,能夠解決子問題並使用這些答案來解決大問題。但僅當每個子問題都是離散的,即不依賴於其它子問題時,動態規劃才管用。

java實現

案例一:

/**
 * 動態規劃 - 簡單揹包問題
 * @author Administrator
 *
 */
public class KnapsackProblem {
	
	public static void main(String[] args){

		float knapsackWeight = 4;
		float[] itemsWeights = new float[] {1, 4, 3};
		float[] itemsPrices = new float[] {1500, 3000, 2000}; 
		float[][] table = knapsackSolution(knapsackWeight, itemsWeights, itemsPrices);
		
		for (int line = 0; line < table.length; line++ ) {
			System.out.println("-----------------line =" + line);
			for (int colume = 0; colume < table[line].length; colume++ ) {
				System.out.println(table[line][colume] + ",");
			}
		}
	}
	
	/**
	 * 
	 * @param knapsackWeight 揹包總容量
	 * @param itemsWeights	  各個物品的所佔據的容量
	 * @param itemsPrices	  各個物品所具有的價值	
	 * @return
	 */
	public static float[][] knapsackSolution(float knapsackWeight, float[] itemsWeights, float[] itemsPrices) {
		if (itemsWeights.length != itemsPrices.length) {
			throw new IllegalArgumentException();
		}
		
		//計算表格的行數 --物品數量
		int lines = itemsPrices.length;
		//計算出劃分的揹包最小的空間,即表格每一列代表的重量為  column * minWeight
		float minWeight = getMinWeight(itemsWeights);
		//計算表格的列數 --分割出的重量數目
		int colums = (int) (knapsackWeight/minWeight);
		System.out.println("lines = " + lines + ",colums = " + colums + ",minWeight = " + minWeight);
		//建立表格物件,lines行colums列
		float[][] table = new float[lines][colums];
	
		for (int line = 0; line < lines; line++ ) {
			for (int colume = 0; colume < colums; colume++ ) {
				
				float left = table[(line - 1) < 0 ? 0 : (line - 1) ][colume];
				float right = 0;
				//判斷當前劃分的小揹包是否可以裝下該物品,當前揹包容量為(colume +1)*minWeight
				if ((colume +1)*minWeight >= itemsWeights[line]) {
					//獲取當前揹包剩餘空間
					float freeWeight = ((colume+1)*minWeight - itemsWeights[line]);
					//判斷剩餘空間是否還可以裝下其它的東西
					int freeColumn = (int) (freeWeight/minWeight) - 1;
					if (freeColumn >= 0 && line > 0) {
						//因為表格上同一列的位置上,越往下價值越高,所以這邊直接取的上一行的freeColumn位置就行
						right = itemsPrices[line] + table[line - 1][freeColumn];
					}else {
						right = itemsPrices[line];
					} 	
				}

				table[line][colume] = Math.max(left, right);
			}
			
		}
		return table;
		
	}
	
	/**
	 * 獲取所有物品中最小的重量
	 * 
	 * @param itemsWeights 各個物品的所佔據的容量
	 * @return
	 */
	public static float getMinWeight(float[] itemsWeights) {
		float min = itemsWeights[0];
		
		for (float weight : itemsWeights) {
			min = min > weight ? weight : min;
		}
		//保留最多2位小數,並預設非零就進位1.222 --> 1.23
		//為啥String.valueOf,參照https://www.cnblogs.com/LeoBoy/p/6056394.html
		return new BigDecimal(String.valueOf(min)).setScale(2, RoundingMode.UP).floatValue();
	}
	
}
複製程式碼
執行完main方法列印資訊如下:
lines = 3,colums = 4,minWeight = 1.0
-----------------line =0
1500.0,
1500.0,
1500.0,
1500.0,
-----------------line =1
1500.0,
1500.0,
1500.0,
3000.0,
-----------------line =2
1500.0,
1500.0,
2000.0,
3500.0,
複製程式碼

簡單修改為,即為案例二的實現程式碼: float knapsackWeight = 2; float[] itemsWeights = new float[] {0.5f,0.5f,1,2,0.5f}; float[] itemsPrices = new float[] {7,6,9,9,8};