演算法設計模式之動態規劃
基本概念
動態規劃(Dynamic programming,簡稱DP)演算法的原理是將問題分成小問題,先解決這些小問題,再逐步解決大問題。推薦參考資料2,以漫畫的形式生動講述了什麼是動態規劃。
動態規劃常常適用於有重疊子問題和最優子結構性質的問題,動態規劃方法所耗時間往往遠少於樸素解法。動態規劃只能應用於有最優子結構的問題。最優子結構的意思是區域性最優解能決定全域性最優解(對有些問題這個要求並不能完全滿足,故有時需要引入一定的近似)。簡單地說,問題能夠分解成子問題來解決。
動態規劃中包含三個重要的概念:
- 最優子結構
- 邊界
- 狀態轉移公式
揹包問題
我們以揹包問題來學習如何設計問題的動態規劃解決方案。假設往一個可裝4磅東西的揹包裡裝東西,可裝的東西如下,如何是揹包裡的東西價值最高。
如果嘗試各種可能的商品組合,然後找出價值最高的組合,演算法執行的時間為O(2^n),真的會慢如蝸牛。下面來演示動態規劃演算法的執行過程。
每個動態規劃演算法都從一個網格開始,揹包問題的網格如下。
網格的各行為商品,各列為不同容量(1~4磅)的揹包。所有這些列你都需要,因為它們將幫助你計運算元揹包的價值。網格最初是空的,我們將填充其中的每個網格,網格填滿後,就找到了問題的答案。首先我們填充吉他列。第一個單元格表示揹包的容量為1磅。吉他的重量也是1磅,這意味著它能裝入揹包!因此這個單元格包含吉他,價值為1500美元。第二個單元格表示揹包的容量為2磅。吉他的重量是1磅,這意味著它能裝入揹包!因此這個單元格包含吉他,價值為1500美元。以此類推,結果如下:
然後我們填充音響行,這行可裝的商品有吉他和音響。在每一行,可裝的東西都為當前行的東西以及之前各行的東西。我們先來看第一個單元格,它表示容量為1磅的揹包, 裝不下音響。在此之前,可裝入1磅揹包的商品的最大價值為1500美元。第二個網格2磅,第三個網格3磅,最大價值都為1500美元。第四個網格為4磅,可以裝下音響,因此最大價值變為3000美元。結果如下:
最後我們填充膝上型電腦行,膝上型電腦重3磅,沒法將其裝入容量為1磅或2磅的揹包,因此前兩個單元格的最大價值還是1500美元。對於容量為3磅的揹包,原來的最大價值為1500美元,但現在你可選擇盜竊價值2000美元的膝上型電腦而不是吉他,這樣新的最大價值將為2000美元!。結果如下圖。對於容量為4磅的揹包,情況很有趣。這是非常重要的部分。當前的最大價值為3000美元,
你可不偷音響,而偷膝上型電腦,但它只值2000美元。價值沒有原來高。但等一等,膝上型電腦的重量只有3磅,揹包還有1磅的容量沒用!在1磅的容量中,可裝入的商品的最大價值是1500美元。因此音響 <(膝上型電腦+ 吉他)。最終的網格類似下面這樣。
我們總結會發現,其實計算每個單元格的價值時,使用的公式都相同,如下:
示例演示
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
struct BagResult
{
int sum_value = 0; //最終揹包裡物品的總價值
int sum_weight = 0; //最終揹包裡物品的總價值
vector<string> names;
};
struct Goods //表示每件物品
{
string name;
int weight;
int value;
Goods(const string& name, int weight, int value)
{
this->name = name;
this->weight = weight;
this->value = value;
}
};
int main()
{
int total_weight = 4; //揹包最多能裝的重量
/* 物品名稱: 音響 膝上型電腦 吉他
* 重量 : 4磅 3磅 1磅
* 價值 : 3000美元 2000美元 1500美元
*/
vector<Goods> goodslist;
goodslist.push_back({"音響", 4, 3000});
goodslist.push_back({"吉他", 3, 1500});
goodslist.push_back({"膝上型電腦", 1, 2000});
if(goodslist.empty())
return 0;
//動態規劃
BagResult* preresult = new BagResult[total_weight];
BagResult* result = new BagResult[total_weight];
//填充第一行
for(int i = 0; i < total_weight; i++){
int w = i + 1; //當前列的重量
if(w < goodslist[0].weight){
preresult[i].sum_weight = 0;
preresult[i].sum_value = 0;
preresult[i].names.clear();
}
else {
preresult[i].sum_weight = goodslist[0].weight;
preresult[i].sum_value = goodslist[0].value;
preresult[i].names = {goodslist[0].name};
}
cout << preresult[i].sum_value << " ";
}
cout << endl;
for(int i = 1; i < goodslist.size(); i++){
for(int j = 0; j < total_weight; j++){
int w = j + 1; //當前列的重量
if(w < goodslist[i].weight)
result[j] = preresult[j];
else if(w == goodslist[i].weight) {
if(preresult[j].sum_value > goodslist[i].value)
result[j] = preresult[j];
else{
result[j].sum_weight = goodslist[i].weight;
result[j].sum_value = goodslist[i].value;
result[j].names = {goodslist[i].name};
}
}
else {
if(preresult[j].sum_value > (preresult[w - goodslist[i].weight - 1].sum_value + goodslist[i].value))
result[j] = preresult[j];
else{
result[j] = preresult[w - goodslist[i].weight - 1];
result[j].sum_weight += goodslist[i].weight;
result[j].sum_value += goodslist[i].value;
result[j].names.push_back(goodslist[i].name);
}
}
cout << result[j].sum_value << " ";
}
for(int i = 0; i < total_weight; i++){
preresult[i] = result[i];
}
cout << endl;
}
cout << "Dynamic Programming Algorithm Result:" << endl;
for(auto& name : result[total_weight - 1].names)
cout << name.c_str() << endl;
cout << result[total_weight - 1].sum_value << endl;
//release sources
delete []preresult;
delete []result;
system("pause");
}
執行結果
總結
動態規劃無法處理裝商品的一部分,例如裝一袋大米的一部分。這種情況使用貪婪演算法可以輕鬆地處理。另外,動態規劃功能強大,能夠解決小問題並使用這些答案來解決大問題,但僅當每個子問題都是離散,即不依賴其他子問題時,動態規劃才管用。所以解決不了,當吉他裝入揹包,音響價值將會減少100美元的問題。
需要在給定約束條件下優化每種指標時,動態規劃很有用的。每種動態規劃解決方案都涉及網格。
參考資料
- 《演算法圖解》[M]
- 什麼是動態規劃(https://zhuanlan.zhihu.com/p/31628866)