有人說:越炫耀什麼,越缺少什麼。但我卻以爲:越缺少什麼,越覺得別人炫耀什麼。 ——李宮俊《李宮俊的詩》
0. 前言
參考圖書《算法導論》
動態規劃通常用來解決最優化問題,在這類問題中,我們通常做出一組選擇來表達最優解。在做出這個選擇的同時,通常會生成與原問題形式相同的子問題。當多於一個選擇子集都生成相同的子問題時,動態規劃技術通常很有效,其關鍵技術就是對每一個這樣的子問題都保存其解,當其重複出現的時候即可避免重複求解。這種思想可以將指數時間的算法轉換爲多項式時間的算法。
動態規劃(dynamic programming)是運籌學的一個分支,是求解決策過程(decision process)最優化的數學方法。動態規劃(dynamic programming)中的programming指的是一種表格法,不是編寫計算機程序。
1. 動態規劃解析
採用動態規劃求解的問題的一般要具有3個性質:
(1) 最優化原理:如果問題的最優解所包含的子問題的解也是最優的,就稱該問題具有最優子結構,即滿足最優化原理。
(2) 無後效性:即某階段狀態一旦確定,就不受這個狀態以後決策的影響。也就是說,某狀態以後的過程不會影響以前的狀態,只與當前狀態有關。
(3)有重疊子問題:即子問題之間是不獨立的,一個子問題在下一階段決策中可能被多次使用到。(該性質並不是動態規劃適用的必要條件,但是如果沒有這條性質,動態規劃算法同其他算法相比就不具備優勢)。
動態規劃求解基本步驟:
1. 刻畫一個最優解的結構特徵。
2. 遞歸的定義最優解的值。
3. 計算最優解的值,通常採用自底向上的方法。
4. 利用計算出的信息構造一個最優解。
2. 動態規劃的應用
2.1 鋼條切割
問題陳述:給定一個長度爲
長度 |
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|
價格 |
1 | 5 | 8 | 9 | 10 | 17 | 17 | 20 | 24 | 30 |
問題分析:從題目中我們可以得出給定的長度爲
更一般的對於
注意,爲了求解規模爲n的原問題,我們求解形式完全一樣(最優解結構特徵刻畫),完成首次切割之後我們將兩段鋼條看作兩個獨立的鋼條切割問題,通過組合兩個相關子問題的最優解(最優子結構),並且在所有可能的兩段切割方案中選取組合收益最大者,構成原問題的最優解。
除了上述求解方式還有一種遞歸求解的方式公式如下:
算法設計:
自頂向下的普通遞歸算法設計:
SteelCut(p[],n){
//初次判斷
if(n==0){
return 0;
}
int q=-1;
//不做切割
if(n<=p.length){
q=p[n];
}
//遞歸求解
for(i=1 to n){
q=max(q,p[i]+SteelCut(p[],n-i));
}
return q;
}
使用動態規劃的算法設計:
//算法1:帶備忘錄的自頂向下法
MemorizedSteelCut(p[],n){
int r[] = new int[n];
for(i=0 to n){
r[i]=-1;
}
return MemorizedSteelCutAux(p[],n,r);
}
MemorizedSteelCutAux(p[],n,r){
if(r[n]>=0){
return r[n];
}
if(n==0){
q=0;
}
else{
q=-1;
for(i=1 to n){
q=max(q,p[i]+MemorizedSteelCutAux(p[],n-i,r));
}
}
r[n]=q;
return q;
}
//算法2:自底向上法
BottomUpSteelCut(p[],n){
int r[] = new int[n];
r[0]=0;
for(j=1 to n){
q=-1;
for(i=1 to j){
q=max(q,p[i]+r[j-i]);
}
r[j]=q;
}
}
Java實現:
package lbz.ch15.dp.ins1;
/**
* @author LbZhang
* @version 創建時間:2016年3月4日 下午2:20:33
* @description 鋼條切割問題
*/
public class SteelCutting {
public static void main(String[] args) {
System.out.println("DP 在鋼條切割問題中的應用 ");
int price[] = {1,5,8,9,10,17,17,20,24,30};
int n = 10;
//自頂向下的遞歸實現
int result = 0;
result = TopToBottomRecursion(price,n);
System.out.println("常規的思路:"+result);
//使用動態規劃來實現 備忘錄
result=MemorizedSteelCut(price,n);
System.out.println("備忘錄法:"+result);
//使用動態規劃的自底向上非遞歸的實現
result=BottomToTopSteelCut(price,n);
System.out.println("自底向上非遞歸方法:"+result);
}
private static int BottomToTopSteelCut(int[] price, int n) {
int r[] = new int[n+1];
r[0]=0;//動態表的開頭
for(int j=1;j<=n;j++){
int q=-1;
for(int i=1;i<=j;i++){
q=maxOfTwo(q,price[i-1]+r[j-i]);
}
r[j]=q;
}
return r[n];
}
/**
* 備忘錄方法
* @param price
* @param n
* @return
*/
private static int MemorizedSteelCut(int[] price, int n) {
int r[] = new int[n+1];
for(int i=0;i<=n;i++){
r[i]=-1;
}
return MemorizedSteelCutAux(price,n,r);
}
/**
* 輔助過程的備忘錄核心算法
* @param price
* @param n
* @param r
* @return
*/
private static int MemorizedSteelCutAux(int[] price, int n, int[] r) {
if(r[n]>=0){
return r[n];
}
int q=0;
if(n==0){
q=0;
}else{
q=-1;
for(int i=1;i<=n;i++){
//price[i-1] 應爲price的下標是從0開始,
q=maxOfTwo(q,price[i-1]+MemorizedSteelCutAux(price,n-i,r));
}
}
r[n]=q;
return q;
}
/**
* //自頂向下的遞歸實現 常規思路
* @param price ``````````````````````````````````````````````````````````````````
* @param n
* @return
*/
private static int TopToBottomRecursion(int[] price, int n) {
if(n==0) return 0;
int q = -1;
if(n<=price.length){
q=price[n-1];
}
for(int i=1;i<n;i++){
q=maxOfTwo(q,price[i-1]+TopToBottomRecursion(price,n-i));
}
return q;
}
private static int maxOfTwo(int x, int y) {
return x>y?x:y;//三目運算符的使用
}
}
重構解-對源程序進行修改
private static int BottomToTopSteelCut(int[] price, int n) {
int r[] = new int[n+1];
int s[] = new int[n+1];
r[0]=0;//動態表的開頭
for(int j=1;j<=n;j++){
int q=-1;
for(int i=1;i<=j;i++){
//q=maxOfTwo(q,price[i-1]+r[j-i]);
if(q<price[i-1]+r[j-i]){
q=price[i-1]+r[j-i];
s[j]=i;
}
}
r[j]=q;
}
System.out.println();
for(int temp=0;temp<=n;temp++){
System.out.print(s[temp]+"|-"+temp+"-|");
}
System.out.println();
//正確的組合輸出
printToFormal(s);
return r[n];
}
private static void printToFormal(int[] s) {
int len=s.length-1;
int temp=s[len];
System.out.print("鋼條切割的組合方式: "+temp+" ");
while(temp!=len){
len=len-temp;
temp=s[len];
System.out.print("+ "+temp+" ");
}
System.out.println();
}
2.2 斐波那契數列
下面直接給出斐波那契數列的Java實現的
package lbz.ch15.dp.ins1;
/**
* @author LbZhang
* @version 創建時間:2016年3月7日 下午9:42:15
* @description 類說明
*/
public class MemoryAndTable {
static int MAX = 20;
static int[] lookUp = new int[MAX];
public static int fibMemory(int n) {
if (lookUp[n] == 0) {
if (n <= 1) {
lookUp[n] = n;
} else {
lookUp[n] = fibMemory(n - 1) + fibMemory(n - 2);
}
}
return lookUp[n];
}
// //打表(自下而上)
public static int fibTable(int n) {
int[] f = new int[n + 1];
int i;
f[0] = 0;
f[1] = 1;
for (i = 2; i <= n; i++) {
f[i] = f[i - 1] + f[i - 2];
}
return f[n];
}
public static void main(String[] args) {
int n = 5;
System.out.println(fibMemory(9));
System.out.println();
// int res=0;
// res=fibTable(9);
System.out.println(fibTable(9));
}
}
注意:在動態規劃中,子問題解決方案被存儲在一個表中,以便這些不必重新計算。 因此,如果這個問題是沒有共同的(重疊)子問題, 動態規劃是沒有用的。例如,二分查找不具有共同的子問題。