動態規劃是一種算法,經過將複雜問題分解爲子問題來解決給定的複雜問題,並存儲子問題的結果,以免再次計算相同的結果。java
如下是一個問題的兩個主要特性,代表可使用動態規劃解決給定的問題。算法
像分而治之同樣,動態規劃結合了子問題的解決方案。 動態規劃主要用於解決一次又一次須要計算相同子問題的複雜問題。 在動態規劃中,子問題的計算解決方案存儲在一個表中,這樣就沒必要從新計算。 因此當沒有共同的(重疊的)子問題時,動態規劃是沒有用的。例如,二分搜索沒有共同的子問題。 若是咱們以斐波納契數的遞歸程序爲例,有許多子問題一次又一次地被解決。數組
/* 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)的值,那麼不用再次計算它,而是能夠從新使用舊的存儲值。 有如下兩種不一樣的方式來存儲值,以便這些值能夠重複使用:優化
一個問題的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));
}
}
複製代碼
給定問題的表格程序以自下而上的方式構建一個表,並從表中返回最後一個條目。 例如,對於相同的斐波納契數,咱們首先計算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
給定問題具備最優子結構性質,若是給定問題的最優解能夠經過使用子問題的最優解獲得。 例如,最短路徑問題具備如下最佳的子結構屬性:若是節點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);
}
複製代碼
// 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);
}
複製代碼
正如名字自己所暗示的,從底部開始,積累到頂部的答案。 讓咱們從狀態轉換的角度來討論。 讓咱們將DP問題的狀態描述爲dp[x],其中dp[0]爲基態,dp[n]爲目標狀態。 因此,咱們須要找到目標狀態的值,即dp[n]。 若是咱們從基態dp[0]開始轉換而且跟隨咱們的狀態轉換關係到達咱們的目標狀態dp[n],咱們稱之爲自下而上方法,由於咱們很清楚地開始了從最底部 狀態並達到最理想狀態。
咱們從最高的目標狀態開始,並經過計算能夠達到目的地狀態的狀態的值來計算它的答案,直到咱們達到最底層的基本狀態。
每一個動態規劃算法都從一個網格開始,揹包問題的網格以下:
其中吉他價值1500,佔容量1,筆記本電腦價值2000,佔容量3,音響價值3000,佔容量4。
網格的各行爲商品,各列爲不一樣容量(1~4磅)的揹包。
第一個單元格表示揹包的容量爲1磅。吉他的重量也是1磅,這意味着它能裝入揹包!所以這個單元格包含吉他,價值爲1500美圓。
音響行
你如今出於第二行,可偷的商品有吉他和音響。在每一行,可偷的商品都爲當前行的商品以及以前各行的商品
計算每一個單元格的價值時,使用的公式都相同。這個公式以下。
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());
}
}
複製代碼
繪製表格
考慮三個問題:單元格中的值是什麼?如何將這個問題劃分爲子問題?網格的座標軸是什麼?
單元格中的值一般就是你要優化的值。在這個例子中爲:兩個字符串都包含的最長子串的長度
。
假設比較fish和hish。
答案爲網格中最大的數字。
兩個單詞中都有的序列包含的字母數
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"));
}
}
複製代碼