基本概念
动态规划(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)