暫先不看問題自己,先來了解一下什麼叫動態規劃。從英文的dynamic programming來看彷佛並無「規劃」的意思在裏邊。可是,這裏的programming並不是指的是編程,而是指的一種表格法,這種表格法旨在一步步詳細分解問題,使之細化並最終得到問題的解。因此咱們稱之爲「規劃」。ios
動態規劃和分治法相似,都是將大問題分解成小的子問題。但分治法自己的小問題每每是獨立的,而動態規劃的小的子問題依賴於大問題。算法
動態規劃方法一般用來求解最優化問題(optimization problem)。這類問題一般能夠有不少個解,例如鋼條切割能夠有不少中切割方法一樣達到最大收益。編程
一般按以下4個步驟設計一個動態規劃算法:
1. 刻畫一個最優解的結構特徵。
2. 遞歸定義最優解的值
3. 計算最優解的值,一般採用自底向上的方法。
4. 利用計算出的信息構造一個最優解。
函數
長度爲n英寸的鋼條共有2^n-1種不一樣的方法(這裏認爲對稱切割屬於不一樣的方法)。工具
這裏以n = 9 爲例,則總共有256種切割方案。優化
分別查看幾種方案:設計
第一種:code
收益: 1 + 20 = 21遞歸
第二種:內存
收益: 10 + 10 = 20 比第一種少1
第三種:
收益: 1 * 9 = 9 顯然是收益最少的
綜合來看,收益最大的應該是 17 + 8 = 25,即分紅6和3
算法實現:
1. 自頂向下遞歸實現
2. 帶備忘的自頂向下法
3. 自底向上法
幾種方法的比較:
切鋼條只是一個引子,切的過程就是對應動態規劃的不一樣的規模下子問題的求解過程。用不用遞歸併非動態規劃的本質。遞歸只是一種方法或者工具,而不是一種思想。自底向上的方法就沒有用到函數遞歸。
樸素的遞歸算法之因此效率很低,是由於它反覆求解相同的子問題。比方長度爲33的鋼條能夠有2^32 = 4294967296種切割方法。用樸素的遞歸方法,須要求解這麼大的一個規模,且不說頻繁調用函數所產生的花銷,要計算10億次以上的加法和比較,這自己就很消耗時間。
基於此,動態規劃方法仔細地安排求解順序,對每個子問題都只求解一次,並將值保存起來。若是以後再有求此子問題即可以查詢其值而不是從新再求一遍。帶備忘的動態規劃法須要額外的內存開銷,可是節省的時間倒是可觀的:可能將一個指數時間的解轉化爲多項式時間的解。
C++代碼實現:
#include<iostream> #include<climits> #include<ctime> #include<cstdlib> int price[] = {1,5,8,9,10,17,17,20,24,30,32,33,33,39,41,44,45,45,45,48,50,55,60,70,78,79,79,88,90,91,92,92,92}; int max(int a,int b) { return a>b?a:b; } int cut_rod(int p[],int n) { if(n == 0) return 0; int q = INT_MIN; for(int i = 1; i <= n; i++){ q = max(q,p[i-1] + cut_rod(p,n-i)); } return q; } int cut_rod_memoized(int p[],int n,int r[]) { if(r[n-1] >= 0) return r[n-1]; int q; if(n == 0){ q = 0; } else{ q = INT_MIN; } for(int i=1;i <= n; i++) q = max(q,p[i-1]+cut_rod_memoized(p,n-i,r)); r[n-1] = q; return q; } int cut_rod_memoized_core(int p[],int n) { int r[n]; for(int i=0;i < n;i++){ r[i] = INT_MIN; } return cut_rod_memoized(p,n,r); } int bottom_up_cut_rod(int p[],int n) { int r[n]; for(int j = 1;j <= n; j++){ int q = INT_MIN; for(int i = 1; i <= j; i++){ if(j-1 -i >= 0){ q = max(q,p[i-1] + r[j-1 - i]); } else{ q = max(q,p[i-1]); } } r[j-1] = q; } return r[n-1]; } int main(int argc,char* argv[]) { if(argc != 2) return -1; time_t start_time = time(NULL); int condition = atoi(argv[1]); switch(condition){ case 0:std::cout<<cut_rod(price,sizeof(price)/sizeof(int))<<std::endl; break; case 1:std::cout<<cut_rod_memoized_core(price,sizeof(price)/sizeof(int))<<std::endl; break; case 2:std::cout<<bottom_up_cut_rod(price,sizeof(price)/sizeof(int))<<std::endl; break; default:break; } time_t end_time = time(NULL); std::cout<<"use:"<<end_time-start_time<<"seconds"<<std::endl; return 0; }
單純的遞歸實現須要大概100秒的時間才能算出來,而另兩種只須要不到1秒時間。這就能夠看出動態規劃的威力了。