動態規劃

動態規劃是一種算法,經過將複雜問題分解爲子問題來解決給定的複雜問題,並存儲子問題的結果,以免再次計算相同的結果。java

如下是一個問題的兩個主要特性,代表可使用動態規劃解決給定的問題。算法

  1. 重複子問題
  2. 最佳子結構

重疊子問題:

像分而治之同樣,動態規劃結合了子問題的解決方案。 動態規劃主要用於解決一次又一次須要計算相同子問題的複雜問題。 在動態規劃中,子問題的計算解決方案存儲在一個表中,這樣就沒必要從新計算。 因此當沒有共同的(重疊的)子問題時,動態規劃是沒有用的。例如,二分搜索沒有共同的子問題。 若是咱們以斐波納契數的遞歸程序爲例,有許多子問題一次又一次地被解決。數組

/* simple recursive program for Fibonacci numbers */
int fib(int n) {
    if ( n <= 1 )
    return n;
    return fib(n-1) + fib(n-2);
}
複製代碼

Recursion tree for execution of fib(5)函數

咱們能夠看到函數fib(3)被調用了2次。 若是咱們已經存儲了fib(3)的值,那麼不用再次計算它,而是能夠從新使用舊的存儲值。 有如下兩種不一樣的方式來存儲值,以便這些值能夠重複使用:優化

  1. Memoization(自上而下)
  2. Tabulation(自下而上)

Memoization(自上而下)

一個問題的memoized程序相似於遞歸版本,只是在計算解決方案以前查看一個查找表。 咱們初始化一個全部初始值爲NIL的查找數組。 每當咱們須要解決一個子問題,咱們首先查找查找表。 若是預先計算的值在那裏,那麼咱們返回該值,不然咱們計算該值並將結果放在查找表中,以便稍後能夠從新使用。ui

如下是第n個斐波納契數的Memoization版本。this

public class Fibonacci {
    final int MAX = 100;
    final int NIL = -1;

    int lookup[] = new int[MAX];

    void _initialize() {
        for (int i = 0; i < MAX; i++) {
            lookup[i] = NIL;
        }
    }

    int fib(int n) {
        if (lookup[n] == NIL) {
            if (n <= 1)
                lookup[n] = n;
            else
                lookup[n] = fib(n - 1) + fib(n - 2);
        }
        return lookup[n];
    }

    public static void main(String[] args) {
        // TODO Auto-generated method stub
        Fibonacci f = new Fibonacci();
        int n = 10;
        f._initialize();
        System.out.println(f.fib(n));
    }
}
複製代碼

Tabulation(自下而上)

給定問題的表格程序以自下而上的方式構建一個表,並從表中返回最後一個條目。 例如,對於相同的斐波納契數,咱們首先計算fib(0),而後計算fib(1),而後計算fib(2),而後計算fib(3)等等。 因此從字面上看,咱們正在自下而上地構建子問題的解決方案。spa

如下是第n個斐波納契數字的表格版本。3d

public static int fib(int n) {
        int f[] = new int[n + 1];
        f[0] = 0;
        f[1] = 1;
        for (int i = 2; i <= n; i++) {
            f[i] = f[i - 1] + f[i - 2];
        }
        return f[n];
    }
複製代碼

嘗試如下問題做爲練習。 1)爲LCS(最長公共子序列)問題寫一個Memoized解決方案。 請注意,Tabular解決方案在CLRS書中給出。 2)你如何選擇Memoization和Tabulation?code

Tabulation vs Memoization

最優子結構

給定問題具備最優子結構性質,若是給定問題的最優解能夠經過使用子問題的最優解獲得。 例如,最短路徑問題具備如下最佳的子結構屬性:若是節點x位於從源節點u到目的節點v的最短路徑,那麼從u到v的最短路徑是從u到x的最短路徑和從x到v的最短路徑的組合。標準的 All Pair Shortest Path算法如Floyd-Warshall和Bellman-Ford都是動態規劃的典型例子。 最長路徑問題沒有最佳子結構屬性。

如何解決動態規劃問題

步驟: 肯定是否爲dp問題--->用最少的參數決定一個狀態表達式--->肯定不一樣狀態之間的關係--->使用tabulation或memoization

dp問題通常都會包含一個狀態,即子問題,而子狀態之間如何轉換就是一個關鍵。 什麼是狀態呢?一個狀態能夠被定義爲一組參數,它能夠惟一地標識某個特定的位置或站在給定的問題中。 這組參數應儘量小以減小狀態空間。

例如:在咱們着名的揹包問題中,咱們用兩個參數index和weight定義咱們的狀態,即DP [index] [weight]。 在這裏DP [指數] [權重]告訴咱們,經過從範圍0到指數具備袋裝能力的物品能夠得到的最大利潤是重量。 所以,這裏的參數指標和權重能夠惟一地識別揹包問題的子問題。

因此,咱們的第一步就是在肯定問題是DP問題以後,再爲問題決定一個狀態。

由於咱們知道DP是用計算結果來制定最終結果的。因此,咱們下一步將要找到以前的狀態和目前的狀態之間的關係。 這部分是解決DP問題的最難的部分,須要大量的觀察和練習。 讓咱們經過考慮一個示例問題來理解它

給定3個數字{1,3,5},咱們須要告訴 咱們能夠組成一個數字「N」的總數, 使用給定的三個數字的總和。 (容許重複和不一樣的安排)。

造成6的方法總數是:8

1 + 1 +1 + 1 +1 + 1

1 + 1 +1 + 3

1 + 1 +3 + 1

1 + 3+ 1 + 1

3 + 1+ 1 + 1

3 + 3

1 + 5

5 + 1

dp[n]表示經過使用{1,3,5}做爲元素來造成n的排列的總數。 假設咱們已經知道了dp[1],dp[2],dp[3]...,dp[6]。而咱們但願算dp[7]。 dp[7] = dp[7 - 1] + dp[7 - 3] + dp[7 - 5] dp[7] = dp[6] + dp[4] + dp[2] 故dp[n] = dp[n-1] + dp[n - 3] + dp[n - 5]

int solve(int n){
    if(n < 0)
        return 0;
    if(n == 0)
        return 1;
    return solve(n-1) + sovle(n-3) + solve(n-5);
}
複製代碼

Adding memoization or tabulation for the state

// initialize to -1
int dp[MAXN];
 
// this function returns the number of 
// arrangements to form 'n' 
int solve(int n) { 
  // base case
  if (n < 0)  
      return 0;
  if (n == 0)  
      return 1;
 
  // checking if already calculated
  if (dp[n]!=-1) 
      return dp[n];
 
  // storing the result and returning
  return dp[n] = solve(n-1) + solve(n-3) + solve(n-5);
}
複製代碼

Tabulation vs Memoizatation

Tabulation Method – Bottom Up Dynamic Programming

正如名字自己所暗示的,從底部開始,積累到頂部的答案。 讓咱們從狀態轉換的角度來討論。 讓咱們將DP問題的狀態描述爲dp[x],其中dp[0]爲基態,dp[n]爲目標狀態。 因此,咱們須要找到目標狀態的值,即dp[n]。 若是咱們從基態dp[0]開始轉換而且跟隨咱們的狀態轉換關係到達咱們的目標狀態dp[n],咱們稱之爲自下而上方法,由於咱們很清楚地開始了從最底部 狀態並達到最理想狀態。

Memoization Method – Top Down Dynamic Programming

咱們從最高的目標狀態開始,並經過計算能夠達到目的地狀態的狀態的值來計算它的答案,直到咱們達到最底層的基本狀態。

使用動態規劃解決揹包問題

每一個動態規劃算法都從一個網格開始,揹包問題的網格以下:

其中吉他價值1500,佔容量1,筆記本電腦價值2000,佔容量3,音響價值3000,佔容量4。

網格的各行爲商品,各列爲不一樣容量(1~4磅)的揹包。

  1. 吉他行

第一個單元格表示揹包的容量爲1磅。吉他的重量也是1磅,這意味着它能裝入揹包!所以這個單元格包含吉他,價值爲1500美圓。

  1. 音響行

    你如今出於第二行,可偷的商品有吉他和音響。在每一行,可偷的商品都爲當前行的商品以及以前各行的商品

  1. 筆記本行

計算每一個單元格的價值時,使用的公式都相同。這個公式以下。

揹包問題實現代碼:

BagObject類,表示裝入揹包中的物件

public class BagObject {
        public int capaticy;
        public int value;
 
        public BagObject(int cap, int val) {
            // TODO Auto-generated constructor stub
            this.capaticy = cap;
            this.value = val;
        }
    }
複製代碼
public class PackageProblem {
    private int cap;
    private BagObject[] objs;
    private int[][] dp;

    public PackageProblem(int bagCap, BagObject[] objs) {
        // TODO Auto-generated constructor stub
        cap = bagCap;
        this.objs = objs;
        dp = new int[this.objs.length][cap];
    }

    public int getMaxValue() {
        int nowval = objs[0].value;
        int nowcap = objs[0].capaticy;
        int i, j;
        for (i = 0; i < cap; i++) {
            if (i + 1 >= nowcap && dp[0][i] < nowval) {
                dp[0][i] = nowval;
            }
        }
        for (i = 1; i < this.objs.length; i++) {
            nowcap = objs[i].capaticy;
            nowval = objs[i].value;
            for (j = 0; j < cap; j++) {
                if (j + 1 - nowcap > 0) {
                    dp[i][j] = Math.max(dp[i - 1][j], nowval + dp[i - 1][j + 1 - nowcap]);
                } else {
                    dp[i][j] = Math.max(dp[i - 1][j], nowval);
                }
            }
        }
        return dp[objs.length - 1][cap - 1];
    }

    public static void main(String[] args) {
        // TODO Auto-generated method stub
        BagObject guiter = new BagObject(1, 1500);
        BagObject tap = new BagObject(3, 2000);
        BagObject radio = new BagObject(4, 3000);
        BagObject[] objs = new BagObject[3];
        objs[1] = guiter;
        objs[0] = tap;
        objs[2] = radio;
        PackageProblem pp = new PackageProblem(4, objs);
        System.out.println(pp.getMaxValue());
    }
}
複製代碼

使用動態規劃解決LCS問題

  1. 繪製表格

    考慮三個問題:單元格中的值是什麼?如何將這個問題劃分爲子問題?網格的座標軸是什麼?

    單元格中的值一般就是你要優化的值。在這個例子中爲:兩個字符串都包含的最長子串的長度

    假設比較fish和hish。

2. 填充網格

  1. 公式

答案爲網格中最大的數字。

最長公共子序列

兩個單詞中都有的序列包含的字母數

實現代碼

public class LongCS {
    public static int lcs(String a, String b) {
        int[][] dp = new int[a.length() + 1][b.length() + 1];
        for (int i = 1; i < a.length() + 1; i++) {
            for (int j = 1; j < b.length() + 1; j++) {
                if (a.charAt(i - 1) == b.charAt(j - 1)) {
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                } else {
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
                }
            }
        }
        return dp[a.length()][b.length()];

    }

    public static void main(String[] args) {
        // TODO Auto-generated method stub

        System.out.println(lcs("AGGTAB", "GXTXAYB"));
    }

}
複製代碼
相關文章
相關標籤/搜索
本站公眾號
   歡迎關注本站公眾號,獲取更多信息