動態規划的思想與應用


動態規划在實際應用中十分廣泛,經常在筆試中碰到動態規划的題目,而且理解起來也比較困難,靈活應用起來更加的不容易,今天就總結一下,到底在什么時候使用動態規划,以及怎么使用動態規划。

動態規划的使用場景一般包括三個特征:

   (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。

 

關於動態規划就講到這里啦,我覺得最難的地方在於怎么把問題分階段的考慮,以及推導出遞推式子。

如有錯誤,歡迎指正。

 
        

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM