動態規划在實際應用中十分廣泛,經常在筆試中碰到動態規划的題目,而且理解起來也比較困難,靈活應用起來更加的不容易,今天就總結一下,到底在什么時候使用動態規划,以及怎么使用動態規划。
動態規划的使用場景一般包括三個特征:
(1)最優子結構:如果問題的最優解所包含的子問題的解也是最優的,就稱該問題具有最優子結構。
比如要想求棋盤兩對角的最短距離,那個每走一格算一格階段,每一格最優解必定是在上一格的最優解中得來的,也就是說每一個階段都有最優的一個決策代表最優子解。
(2)無后效性:某一狀態一旦確定,就不受這個狀態的以后的決策影響。
后效性是指,如果上面的例子中的每一個格子的長度是不一致的,那么在當前選擇的格子決策背景下,可能會對后面選擇決策造成影響,如下圖。
如果選擇了當前選擇了值為10的這個節點,則會導致后續只能選擇100,甚至200的節點,即在選擇10和20節點的時候,會對后續的決策產生影響。
(3)有重疊子問題:子問題之間不是相互獨立的,一個子問題在下一階段的決策中可能被多次使用到。
即當前的最優解的計算,我們只需要記錄下來,在后續的比較中使用到,而不需要再次計算,增加時間復雜度。
動態規划的基本過程描述:
每次決策依賴於當前狀態,又引起狀態的轉移,一個決策序列就是在變化的狀態中產生出來的,所以,動態規划可以看成是多階段最優化決策來解決問題的過程。
使用動態規划解決問題,最重要的就是確定動態的三要素:
(1)問題的階段。
(2)每個階段的狀態。
(3)從前一個階段到后一個階段的遞推關系。
下面我們看一個經典而簡單的例子來闡述一下,動態規划到底怎么用來解決問題:
1 package DP; 2 3 /** 4 * Created by jy on 2017/10/5. 5 * 求解給定數組的子數組,使得子數組的和最大,子數組的元素在給定的數組中必須是連續的 6 */ 7 public class MaxSumSubArray { 8 9 /*dp解法: 10 * 可以令curMax[]是以當前元素結尾的最大子數組和,maxSum是全局的最大子數組和, 11 * 當往后掃描時,對第j個元素有兩種選擇:要么放入前面找到的子數組,要么做為新子數組的第一個元素 12 * 舉個例子,當輸入數組是1, -2, 3, 10, -4, 7, 2, -5,那么,currSum和maxSum相應的變化為: 13 * 14 * j(前j個元素): 0 1 2 3 4 5 6 7 8 15 * currSum[] : 0 1 -1 3 13 9 16 18 13 16 * maxSum[] : 0 1 1 3 13 13 16 18 18 17 * 18 * 19 * 1.起始階段(i=0),max = nums[0]; 20 * 2.第i(i > 0)個階段,max = curMax[i],curMax是第i個階段的最大子序列和; 21 * 3.第i-1和第i個階段的關系,curMax[i] = Math.max(curMax[i - 1] + nums[i], nums[i]); 22 * 4.根據前面動態規划的定義,則最大子序列和max = Math.max(max, curMax[i]) 23 * */ 24 public static int DpMaxSubArray(int[] nums) { 25 //curMax是當前的最大子序列和 26 int[] curMax = new int[nums.length]; 27 //起始階段 28 curMax[0] = nums[0]; 29 //刻畫最優解 30 int max = nums[0]; 31 for (int i = 1; i < nums.length; i++) { 32 //每一階段的最優都有上一階段的最優的基礎上而來 33 curMax[i] = Math.max(curMax[i - 1] + nums[i], nums[i]); 34 System.out.print(curMax[i] + " "); 35 max = Math.max(max, curMax[i]); 36 } 37 return max; 38 } 39 }
通常動態規划都有一個階段的概念,在這里的階段就是前多少個元素,求前兩個元素的最大子數組就是一個階段,求前三個元素的最大子數組是一個階段......每一個階段的最優解都放在一個數組里記錄,用於在下一階段中比較,
在這個問題中,如果第n-1 階段的最大和比第n個元素還要小,則當前的第n個元素作為最優子結構(最大和子數組)的開頭第一個元素,繼續往后掃描,而最大和就在currMax[i]中產生。
我們再來看看一個經典的0-1背包問題:
求解背包問題:
* 給定 n 個物品,其重量分別為 w1,w2,……,wn, 價值分別為 v1,v2,……,vn
* 要放入總承重為 totalWeight 的箱子中,
* 求可放入箱子的背包價值總和的最大值。
1 class ZeroOneProblem { 2 3 //定義背包為item集合,item有兩個屬性,一個是weight,一個是value 4 private Item[] item; 5 6 //總重量 7 private int totalWeight; 8 9 //物品數量 10 private int n; 11 12 //裝n個物品,總重量為totalWeight的最優解 13 private int[][] bestValues; 14 15 //裝n個物品,總重量為totalWeight的最優值 16 private int bestValue; 17 18 //裝n個物品,總重量為totalWeight的最優時的物品組成 19 private ArrayList<Item> bestSolution; 20 21 // 求解前 n 個背包、給定總承重為 totalWeight 下的背包問題 22 public void solve() { 23 System.out.println("給定物品:"); 24 for (Item item : item) { 25 System.out.println(item); 26 } 27 System.out.println("給定總承重: " + totalWeight); 28 //i為0時,表示沒有物品,j為0時表示沒有重量 29 for (int j = 0; j <= totalWeight; j++) { 30 for (int i = 0; i <= n; i++) { 31 if (i == 0 || j == 0) { 32 bestValues[i][j] = 0; 33 } else { 34 // 如果第 i 個物品重量大於此時的剩下的承重,則最優解存在於前 i-1 個物品中,第i個物品不放進去 35 // 第 i 個物品是 item[i-1] 36 if (j < item[i - 1].getWeight()) { 37 bestValues[i][j] = bestValues[i - 1][j]; 38 } else { 39 // 如果第 i 個物品不大於總承重,則最優解要么是包含第 i 個物品的最優解, 40 // 要么是不包含第 i 個物品的最優解, 取兩者最大值。 41 // 第 i 個物品的重量 iweight 和價值 ivalue 42 int iweight = item[i - 1].getWeight(); 43 int ivalue = item[i - 1].getValue(); 44 bestValues[i][j] = 45 Math.max(bestValues[i - 1][j], ivalue + bestValues[i - 1][j - iweight]); 46 } 47 } 48 } 49 } 50 51 // 回溯:求解背包組成 52 if (bestSolution == null) { 53 bestSolution = new ArrayList<Item>(); 54 } 55 int tempWeight = totalWeight; 56 for (int i = n; i >= 1; i--) { 57 if (bestValues[i][tempWeight] > bestValues[i - 1][tempWeight]) { 58 bestSolution.add(item[i - 1]); // item[i-1] 表示第 i 個物品 59 tempWeight -= item[i - 1].getWeight(); 60 } 61 if (tempWeight == 0) { 62 break; 63 } 64 } 65 bestValue = bestValues[n][totalWeight]; 66 }
如果還是理解不了具體是怎么做的,可以看下面的決策表的生成過程 ,物品如下:
[weight: 2 value: 13]
[weight: 1 value: 10]
[weight: 3 value: 24]
[weight: 2 value: 15]
[weight: 4 value: 28]
[weight: 5 value: 33]
[weight: 3 value: 20]
[weight: 1 value: 8]
totalWeight = 5時,bestValues[6][9](只有藍色部分)數組的產生過程:
詳解一下紅色的34是怎么得到的:此時背包重量為4,只能選擇前3個物品,且當前物品重量為3,小於背包的承重4,bestValues[3][4] = max{ bestValues[2][4] , 24+bestValues[2][1] } , 比較的是不放編號3的物品時最大value和放編號3的物品時的最大value。
關於動態規划就講到這里啦,我覺得最難的地方在於怎么把問題分階段的考慮,以及推導出遞推式子。
如有錯誤,歡迎指正。