1. 程式人生 > >開心的小明(java版) 動態規劃

開心的小明(java版) 動態規劃

淺談DP演算法(一)

                  ——如何用一維陣列解決01揹包問題

 

  DP演算法(Dynamic Programming,俗稱動態規劃)是最經典演算法之一.本筆記以耳熟能詳的數塔問題為引子,深入討論01揹包的解決方法.

  首先,如下圖所示,要求從頂層走到底層,若每一步只能走到相鄰的結點,則經過的結點的數字之和最大是多少?

                                                                       

  這個問題,對於任意一個結點,直接選擇數字大的子結點顯然是不行的.以9為例,如果選擇15,當前和24>21,但是15的兩個子結點太小,24+6+18<21+10+18.由此可見,這樣求出每階段的部分最優解並不是全域性最優解.另外,如果用蠻力演算法,每條路徑都遍歷一次,那麼層數為n時,路徑總數呈指數級增長:

  顯然這種方法的計算量太大,也不可取.那麼此時用DP演算法是行之有效的.具體思想為:從倒數第二層開始,一層一層向上遍歷.倒數第二層第一個結點是2,如果路徑經過2,那麼肯定會選擇數值較大左子結點19. 便用19+2=21代替原先的2. 同理18改為18+10=28,9改為19,5改為21. 這樣倒數第二層就變成21 28 19 21四個結點,再將最後一層捨棄.這樣一層層向上,直到第一層,選擇第二層較大的那個結點加到9上面去,就得出了全域性最優解.

 程式碼實現:如果數字塔為n層,開闢一個n*n的二維陣列即可,非常簡單,此處省略.

  對動態規劃的思想有一個基本瞭解後,現總結出動態規劃基本概念.不過在此之前,首先解釋一下什麼是多階段決策問題,什麼是狀態.

  多階段決策問題:一個問題可以分為若干個階段,每個階段都要做出決策;

  狀態:每個階段開始面臨的自然狀況或客觀條件.在上例中每個階段的狀態就是到達當前結點的兩個子結點的選擇.

  動態規劃是一個多階段決策問題中,各個階段採取的決策,依賴於當前狀態,又引起狀態的轉移,一個決策序列就是在變化的狀態中產生出來的,故有“動態”的含義,稱這種解決多階段決策最優化問題的方法為動態規劃方法.

   適用DP演算法的問題一般具備以下特點:

    (1)最優化原理(最優子結構性質)

    一個最優化策略的子策略也是最優的.舉個例子就清楚了,如數字塔問題中,第二層到底層數值和最大的路徑一定是從頂層到底層數值和最大的路徑的子集;

    (2)無後向性(無後效性)

    通俗講,某階段狀態一旦確定,就不受這個狀態之後的決策的影響;

    (3)子問題重疊

    即子問題之間不獨立.與前兩個不同的是,這個特點不是必要的,如果不滿足相比之下DP演算法不具備優勢.如果獨立,分治演算法策略將更簡單方便.

  下面來看經典的01揹包問題:

    把N個物品放入容量為V的揹包裡,第i個物品所需要的空間為need[i],同時它的價值為value[i],該如何放才能達到揹包裡的物品價值最大?

  分析:因為對於任何一個物品,都只有放或不放的選擇,因而稱之為01揹包.用best(N,V)*表示N個物品放入容量為V的揹包裡的最大價值.則對於第N個物品,除非need[N]>V,都有放或不放兩個選擇:

  (1)如果將第N個物品放入揹包中

best(N,V)=best(N-1,V-need[N])+value[N];  //即等於第N個物品的價值加上將N-1個物品放入容量為V-need[N]的揹包裡的最大價值;

  (2)如果不放

best(N,V)=best(N-1,V-need[N])+value[N];  //即等於第N個物品的價值加上將N-1個物品放入容量為V-need[N]的揹包裡的最大價值;

  這樣我們重複上述步驟,直至N和V減小到0,可以得到對於其中任何一個物品i (1<=i<=N),當前狀態揹包剩餘容量為j (0<=j<=V),都有

if(j<need[i])

    best(i,j)=best(i-1,j);

else

    best(i,j)=MAX{best(i-1,j-need[i])+value[i],best(i-1,j)};

  這就是從第i階段到第i-1階段的狀態轉移規律,程式設計師為了把逼格提高,起了一個術語——狀態轉移方程.

  而當i=0時**,只需設定一下邊界值best(0,j)=0.這樣,求解best(N,V)這個看起來很複雜又無從下手的問題,就變成了從i=0時的best(0,j)=0逐漸到i=N時的best(N,j).

  *best(N,V)只是物品、揹包容量和價值三者之間的關係表示,千萬不要糾結它為什麼這麼表示,到底什麼意思,裡面又是如何根據N,V來得到價值的;

  **本來i=0是沒有意義的,因為是從第N個物品逐漸推導到第1個物品,設定i=0時的best(i,j)只是為了滿足數學上的計算;

 

下面這題目是經典的DP演算法揹包問題的變種:

小明今天很開心,家裡購置的新房就要領鑰匙了,新房裡有一間他自己專用的很寬敞的房間。更讓他高興的是,媽媽昨天對他說:“你的房間需要購買哪些物品,怎麼佈置,你說了算,只要不超過N元錢就行”。今天一早小明就開始做預算,但是他想買的東西太多了,肯定會超過媽媽限定的N元。

於是,他把每件物品規定了一個重要度,分為5等:用整數1~5表示,第5等最重要。他還從因特網上查到了每件物品的價格(都是整數元)。他希望在不超過N元(可以等於N元)的前提下,使每件物品的價格與重要度的乘積的總和最大。

設第j件物品的價格為v[j],重要度為w[j],共選中了k件物品,編號依次為j1,j2,……,jk,則所求的總和為:

v[j1]*w[j1]+v[j2]*w[j2]+ …+v[jk]*w[jk]。(其中*為乘號)

請你幫助小明設計一個滿足要求的購物單。

實現程式碼如下:

package huawei;

public class Demo {
	/*
	 * 功能:
	 * 
	 * 輸入引數:a為二維陣列,該二維陣列第0行的兩個數分別表示:總錢數<30000,和希望購買物品的個數<25;
	 * 該陣列從第1行到第m行(1<=j<=m)中給出了編號為j的物品的基本資料,每行有2個非負整數,
	 * 表示該物品的價格(<=10000)和該物品的重要度(1~5)。
	 * 
	 * GetResult表示不超過總錢數的物品的價格與重要度乘積的總和的最大值(<100000000)。
	 * 
	 * 不需做入參檢查,測試用例可以保證~
	 * 
	 * 例如:4000 8(第0行) 821 3 (第1行) 422 5 458 5 500 3 200 2 430 4 530 3 239 3
	 * 
	 * 則表示 總錢數為4000,希望購買物品個數為8個,因此從第1行到第8行表示編號為j的物品的價格及物品的重要度。
	 * 
	 * 
	 * 
	 * 
	 * 返回值:無
	 * 
	 * 溫馨提示:根據題意可知,該二維陣列只有兩列,且行數為第0行的第二個元素數值+1;
	 */

	public int getResult(int[][] a) {
		// 在這裡實現功能
		int money = a[0][0], numMax = a[0][1];
		int[][] max = new int[a.length][money + 1];
		for (int i = 1; i <= numMax; i++) {
			for (int j = 1; j <= money; j++) {
				if (j >= a[i][0]) {
					max[i][j] = Math.max(max[i - 1][j], max[i - 1][j - a[i][0]] + a[i][0] * a[i][1]);
				} else {
					max[i][j] = max[i - 1][j];
				}
			}
		}
		return max[numMax][money];
	}
}