1、介紹
(1)
動態規劃是解決多階段決策過程最優化的一種有效的數學方法,他是美國學者Richard.bellman在1951年提出的,1957年他的專著《動態規劃》的問世標誌着運籌學的一個重要分支—-動態規劃的誕生。
所謂多階段決策問題是指這樣一類問題,該問題的決策過程時一種在多個相互聯繫的階段分別作出決策以形成序列決策的過程,而這些決策均是根據總體最優化這一共同的目標而採取的。
基本思想:
把一個較複雜的問題按照階段劃分,分解爲若干個較小的局部問題,然後按照局部問題的遞推關係,依次作出一系列決策,直至整個問題達到總體最優的目標。
(2) 動態規劃包含三個重要的概念:
- 最優子結構
- 邊界
- 狀態轉移方程
(3)解題的一般步驟是:
1. 找出最優解的性質,刻畫其結構特徵和最優子結構特徵;
2. 遞歸地定義最優值,刻畫原問題解與子問題解間的關係;
3. 以自底向上的方式計算出各個子問題、原問題的最優值,並避免子問題的重複計算;
4. 根據計算最優值時得到的信息,構造最優解。
(4)使用動態規劃特徵:
1. 求一個問題的最優解
2. 大問題可以分解爲子問題,子問題還有重疊的更小的子問題
3. 整體問題最優解取決於子問題的最優解(狀態轉移方程)
4. 從上往下分析問題,從下往上解決問題
5. 討論底層的邊界問題
2、最長公共子序列(LCS)與最長公共子串(DP)
(1)有兩個母串:
A B C B D A B
B D C A B A
公共子序列:在母串中都出現過並且出現順序與母串保持一致。
最長公共子序列(Longest Common Subsequence,LCS),顧名思義,是指在所有的子序列中最長的那一個。
子串:是要求更嚴格的一種子序列,要求在母串中連續地出現。
(2)求解最長公共子序列
對於母串X=<x1,x2,⋯,xm>, Y=<y1,y2,⋯,yn>,求LCS與最長公共子串。
動態規劃
假設Z=<z1,z2,⋯,zk>是X與Y的LCS, 我們觀察到
如果Xm=Yn,則Zk=Xm=Yn,有Zk−1是Xm−1與Yn−1的LCS;
如果Xm≠Yn,則Zk是Xm與Yn−1的LCS,或者是Xm−1與Yn的LCS。
因此,求解LCS的問題則變成遞歸求解的兩個子問題。但是,上述的遞歸求解的辦法中,重複的子問題多,效率低下。改進的辦法——用空間換時間,用數組保存中間狀態,方便後面的計算。這就是動態規劃(DP)的核心思想了。
DP求解LCS
用二維數組ci記錄串x1x2⋯xi與y1y2⋯yj的LCS長度,則可得到狀態轉移方程。
x: A B C B D A B
y: B D C A B A
最長公共子序列:BDAB
BCAB
BCBA
代碼實現:
public static int lcs(String str1, String str2) {
int len1 = str1.length();
int len2 = str2.length();
int c[][] = new int[len1+1][len2+1];
for (int i = 0; i <= len1; i++) {
for( int j = 0; j <= len2; j++) {
if(i == 0 || j == 0) {
c[i][j] = 0;
} else if (str1.charAt(i-1) == str2.charAt(j-1)) {
c[i][j] = c[i-1][j-1] + 1;
} else {
c[i][j] = max(c[i - 1][j], c[i][j - 1]);
}
}
}
return c[len1][len2];
}
(2)求解最長公共子串
轉移方程:
最長公共子串的長度爲 max(c[i,j]), i∈{1,⋯,m},j∈{1,⋯,n}。
代碼實現:
public static int lcs(String str1, String str2) {
int len1 = str1.length();
int len2 = str2.length();
int result = 0; //記錄最長公共子串長度
int c[][] = new int[len1+1][len2+1];
for (int i = 0; i <= len1; i++) {
for( int j = 0; j <= len2; j++) {
if(i == 0 || j == 0) {
c[i][j] = 0;
} else if (str1.charAt(i-1) == str2.charAt(j-1)) {
c[i][j] = c[i-1][j-1] + 1;
result = max(c[i][j], result);
} else {
c[i][j] = 0;
}
}
}
return result;
}
3、揹包問題
(1)問題
假設現有容量10kg的揹包,另外有3個物品,分別爲a1,a2,a3。物品a1重量爲3kg,價值爲4;物品a2重量爲4kg,價值爲5;物品a3重量爲5kg,價值爲6。將哪些物品放入揹包可使得揹包中的總價值最大?
(2)思路
先將原始問題一般化,欲求揹包能夠獲得的總價值,即欲求前i個物體放入容量爲m(kg)揹包的最大價值ci——使用一個數組來存儲最大價值,當m取10,i取3時,即原始問題了。而前i個物體放入容量爲m(kg)的揹包,又可以轉化成前(i-1)個物體放入揹包的問題。下面使用數學表達式描述它們兩者之間的具體關係。
表達式中各個符號的具體含義。
w[i] : 第i個物體的重量;
p[i] : 第i個物體的價值;
c[i][m] : 前i個物體放入容量爲m的揹包的最大價值;
c[i-1][m] : 前i-1個物體放入容量爲m的揹包的最大價值;
c[i-1][m-w[i]] : 前i-1個物體放入容量爲m-w[i]的揹包的最大價值;
由此可得:
c[i][m]=max{c[i-1][m-w[i]]+pi , c[i-1][m]}
代碼實現:
public class Pack01 {
public int [][] pack(int m,int n,int w[],int p[]){
//c[i][v]表示前i件物品恰放入一個重量爲m的揹包可以獲得的最大價值
int c[][]= new int[n+1][m+1];
for(int i = 0;i<n+1;i++)
c[i][0]=0;
for(int j = 0;j<m+1;j++)
c[0][j]=0;
//
for(int i = 1;i<n+1;i++){
for(int j = 1;j<m+1;j++){
//當物品爲i件重量爲j時,如果第i件的重量(w[i-1])小於重量j時,c[i][j]爲下列兩種情況之一:
//(1)物品i不放入揹包中,所以c[i][j]爲c[i-1][j]的值
//(2)物品i放入揹包中,則揹包剩餘重量爲j-w[i-1],所以c[i][j]爲c[i-1][j-w[i-1]]的值加上當前物品i的價值
if(w[i-1]<=j){
if(c[i-1][j]<(c[i-1][j-w[i-1]]+p[i-1]))
c[i][j] = c[i-1][j-w[i-1]]+p[i-1];
else
c[i][j] = c[i-1][j];
}else
c[i][j] = c[i-1][j];
}
}
return c;
}
/**
* 逆推法求出最優解
* @param c
* @param w
* @param m
* @param n
* @return
*/
public int[] printPack(int c[][],int w[],int m,int n){
int x[] = new int[n];
//從最後一個狀態記錄c[n][m]開始逆推
for(int i = n;i>0;i--){
//如果c[i][m]大於c[i-1][m],說明c[i][m]這個最優值中包含了w[i-1](注意這裏是i-1,因爲c數組長度是n+1)
if(c[i][m]>c[i-1][m]){
x[i-1] = 1;
m-=w[i-1];
}
}
for(int j = 0;j<n;j++)
System.out.println(x[j]);
return x;
}
public static void main(String args[]){
int m = 10;
int n = 3;
int w[]={3,4,5};
int p[]={4,5,6};
Pack01 pack = new Pack01();
int c[][] = pack.pack(m, n, w, p);
pack.printPack(c, w, m,n);
}
}
4、雞蛋和樓的問題
動態規劃解決:
dp[i][j]表示對於i層樓並擁有j個雞蛋時能夠判斷雞蛋質量需要的最少次數;
假如我們在第k層扔下一個雞蛋,則有兩種情況,如果雞蛋沒有損壞則問題相當於我們對於i-k層樓擁有j個雞蛋所需的最少的次數。
如果雞蛋碎了,則問題相當於對於k層樓擁有j-1個雞蛋的最小次數。從而可以得到動態規劃公式:
dp[i][j] = Min( Max( dp[k][j-1], dp[i-k][j] ) ) + 1, k ∈ [1. i)
得到狀態轉移方程:
代碼:
public static int resolve(int eggs, int floors) {
int dp[][] = new int[floors+1][eggs+1];
for(int i = 1; i <= floors; i++) {
dp[i][1] = i-1;
}
for(int i = 1; i <= eggs; i++) {
dp[1][i] = 0;
}
for(int i = 2; i <= floors; i++) {
for(int j = 2; j <= eggs; j++) {
int tmp = Integer.MAX_VALUE;
for(int k = 1; k < i; k++) {
tmp = Math.min(tmp, Math.max(dp[k][j-1], dp[i-k][j]));
}
dp[i][j] = tmp + 1;
}
}
return dp[floors][eggs];
}
5、跳臺階
(1)問題
有一座高度是10級臺階的樓梯,從下往上走,每跨一步只能向上1級或者2級臺階。要求用程序來求出一共有多少種走法?
(2)想法
1級臺階有1種方法;
2級臺階有2種方法;
3級臺階有3種方法;
4級臺階有5種方法;
n級臺階有((n-1)級臺階和(n-2)級臺階)的和。
5.1 遞歸方法
根據上面的想法很容易就能寫出遞歸方式的代碼
public int JumpFloor(int target) {
if(target<1)
return 0;
if(target==1)
return 1;
if(target==2)
return 2;
return JumpFloor(target-1)+JumpFloor(target-2);
}
但是會發現時間和空間複雜度高,能不能進行簡化吶?
5.2 遞歸簡化
要計算F(N),需要計算F(N-1)和F(N-2)的值。依次類推,可以歸納成下面的圖:
可以發現其中的有些相同的參數被重複計算了,如圖相同的顏色被重複計算了:
我們可以通過創建一個哈希表,將不同參數的計算結果保存到哈希表中。
public int JumpFloor(int target,HashMap<Integer,Integer> map) {
if(target<1)
return 0;
if(target==1)
return 1;
if(target==2)
return 2;
if(map.contains(target)){
return map.get(target);
}else{
int value = JumpFloor(target-1)+JumpFloor(target-2);
map.put(target,value);
return value;
}
}
空間複雜度和時間複雜度都爲o(N)。
5.3 動態規劃
d(i)表示有i個臺階時的總共跳法。
可以得到狀態轉移方程:
動態規劃是從上到下分析問題,從下到上解決問題。
代碼:
public int jumpfloor(int target){
if(target<1)
return 0;
if(target==1)
return 1;
if(target==2)
return 2;
int a = 1;
int b = 2;
int temp = 0;
for(int i = 3; i <= target; i++){
tem = a + b;
a = b;
b = temp;
}
return temp;
}
空間複雜度爲o(1)和時間複雜度爲o(N)。
6、國王和金礦
(1)問題
有一個國家發現了5座金礦,每座金礦的黃金儲量不同,需要參與挖掘的工人數也不同。參與挖礦工人的總數是10人。每座金礦要麼全挖,要麼不挖,不能派出一半人挖取一半金礦。要求用程序求解出,要想得到儘可能多的黃金,應該選擇挖取哪幾座金礦?
5個礦的最優選擇,就是(前4座金礦10個工人的挖金數量)和(前4座金礦7工人的挖金數量+第5座金礦的挖金數量)的最大值。
(2)動態規劃
N表示金礦數量,W表示工人數,設金礦的黃金量爲G[],金礦的用工量設爲數組P[]。
得到狀態轉移方程:
public int getMostGold(int n, int w, int[] g, int[] p){
int[] preResults = new int[p.length];
int[] results = new int[p.length];
//填充邊界格子的值
for(int i = 0; i <= n; i++){
if(i < p[0]){
preResults[i] = 0;
}else{
preResults[i] = g[0];
}
}
//填充其餘格子的值,外層循環是金礦數量,內層循環是工人數
for(int i = 0;i < n; i++){
for(int j = 0; j <= w; j++){
if(j < p[i]){
results[j] = preResults[j];
}else{
//實際上就是管不管最後一個金礦的問題
results[j] = Math.max(preResults[j],preResults[j-p[i]] + g[i]);
}
}
preResults = results;
}
return results[n];
}
時間複雜度是 O(n * w),空間複雜度是(w)。