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

算法簡介

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

與貪婪算法區別

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

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

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

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

  • 動態規劃相似搜索或者填表的方式來,具備最優子結構的問題能夠採用動態規劃,不然使用貪婪算法ui

案例

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

案例一

揹包問題:有一個揹包,容量爲4磅 , 現有以下物品code

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

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

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

解決相似的問題能夠分解成一個個的小問題進行解決,假設存在揹包容量大小分爲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) = 四、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};

相關文章
相關標籤/搜索