先給出概述:動態規劃通常是用來解決最優化問題的。最優化問題指的是該類問題有很多可行解,我們希望找到最優解(最大或最小的解),更具體的來說:要得到規模爲n的問題的最優解,那麼會用到規模爲n-1的相似問題的最優解。
下面我們將通過鋼條切割問題來詳細描述什麼是動態規劃,動態規劃又是如何操作的。
鋼條問題描述:給定一段長度爲n的鋼條和它對應的收益表Pi(i=1--n),問如何切割鋼條(切割鋼條本身不計成本)使得收益最大。
我們可以很容易想出用遞歸的方式來求解:
1.分解:假設我們知道最優的方案將在i處進行切割(當然我們不知道,所以用i表示,然後進行一次for循環即可)
那麼問題變爲求解長度爲i及長度爲(n-i)的鋼條的最大收益。
2.解決:遞歸地求解,直到長度爲1那麼方案只有一個。
3.合併:將長度爲i及長度爲(n-i)的鋼條的最大收益相加即爲長度爲n的鋼條的最大收益。
int CutRod(int* p, int n) {
if (n == 0)return 0;
int res = INT_MIN;
for (int i = 1; i <= n; i++) {
res = std::max(res, p[i] + CutRod(p, n - i));
}
return res;
}//自頂向下的遞歸,很容易理解的遞歸。算法時間爲指數增長2的n次方。
上面是簡單遞歸版本的代碼。
分析:我們將該過程與歸併排序時的遞歸進行對比分析點擊打開鏈接,發現最大的不同在於:歸併排序將數組分成兩個數組後,對兩個數組進行排序的過程是不相關的,而該問題將問題分解爲求解長度爲i及長度爲(n-i)的鋼條的最大收益時兩個問題是會重疊的(比如說i=n-i,當然求兩次長度爲i的最大收益就很蠢了)——子問題發生重疊。另外,要想求解長度爲n的最優解,那麼必須知道長度爲n-1的最優解———這種性質叫最優子結構。
我們發現如果我把子問題的最優解保存下來,那麼在反覆求解該子問題最優解時直接用結果將大大降低算法的時間,這就是動態規劃的思想實質——我們並不是只用程序開始時的信息,也保存並使用了程序運行過程中產生對接下來問題求解有利的信息。
以下是自底向上的動態規劃代碼:
int BottomUpCutRodOfDynamicProgram(int* p, int n) {
int res[11];
res[0] = 0;
for (int j = 1; j <= n; j++) {
int temp = INT_MIN;
for (int i = 1; i <= j; i++) {
temp = std::max(temp, p[i] + res[j - i]);
}
res[j] = temp;
}
return res[n];
}//兩個for,於是他的時間複雜度大大降低爲n的平方。
代碼闡述:建立額外的數組res來保存長度爲i(數組下標)的最優解。
那通常在實際操作中我們不僅要求算出最大收益,還需要給出最大收益時的切割方案,於是有以下代碼(包括main函數和輸出)
int ExtendBottomUpCutRodOfDynamicProgram(int* p, int n,int* plan) {
int res[11];
res[0] = 0;
for (int j = 1; j <= n; j++) {
int temp = INT_MIN;
for (int i = 1; i <= j; i++) {
if (temp < p[i] + res[j - i]) {
temp = p[i] + res[j - i];
plan[j] = i;
}
}
res[j] = temp;
}
return res[n];
}//用數組plan來存儲方案。
void PrintPlan(int* plan, int n) {
while (n) {
std::cout << plan[n] << std::endl;
n -= plan[n];
}
}
int main() {
int plan[11];
plan[0] = 0;
int price[11]{ 0,1,5,8,9,10,17,17,20,24,30 };
std::cout << CutRod(price, 5)<<std::endl;
std::cout << BottomUpCutRodOfDynamicProgram(price, 5) << std::endl;
std::cout << ExtendBottomUpCutRodOfDynamicProgram(price, 5, plan)
<< std::endl;
PrintPlan(plan, 5);
}
最後就來總結下動態規劃:
採用動態規劃求解的問題應該具備最優子結構(一個問題的最優解包含其子問題的最優解)和子問題重疊(有相同的子問題要解決)
其通用模式是:
1.求解問題的最優解的第一步是做出一個選擇,(列如,鋼條問題中選擇第一次的切割位置)在做出選擇後會產生一個或多個待解決的子問題。
2.確定所產生的子問題,清楚如何最好滴刻畫子問題空間(通俗的說就是什麼樣的子問題會重疊一定要搞清楚,因爲我們要保存的就是這些重疊的子問題。它是一個線索。)