简述
分治方法将问题划分为互不相交的子问题,递归地求解子问题,再将它们的解组合起来,求出原问题的解。与之相反,动态规划应用于子问题重叠的情况,即不同的子问题具有公共的子子问题。对于分治方法,在处理问题的时候,会反复求解那些公共子子问题。而动态规划算法对每个子子问题只求解一次,将其保存在一个表格中,从而无需每次求解一个子子问题,避免了这种不必要的计算工作。
通俗来讲:就是你去考研,需要考政治,英语,数学和专业课这四门,只有把这四门成绩达到一定分数才行。这样考研这个问题就分解为提高每一门科目的成绩,只有把这些子问题解决了,最终你才能成功读研。因为要对学好每一门课程,所以相对来讲它的开销很大。
应用
动态规划方法通常用来求解最优化问题。这类最优化问题一般来说都有很多的解,每个解都对一个值,我们希望寻找具有最优值(最大值或者最小值)的解。我们称这样的解为问题的一个最优解,因为可能存在多个解都达到最优值。
我们通常按这4个步骤来设计一个动态规划算法:
1. 刻画一个最优解的结构特征
2. 递归地定义最优解的值
3. 计算最优化的值,通常采用自底向上的方法
4. 利用计算的信息构造一个最优解
动态规划原理
运用动态规划方法求解的最优化问题应该具备的两个要素:最优子结构和子问题重叠
最优子结构
用动态规划方法求解最优化问题的第一步就是刻画最优解的结构。如果一个问题的最优解包含其子问题的最优解,我们就称此问题具有最优子结构性质。
例如:无权最短路径问题:找到一条从u到v的边数最少的简单路径。
我们可以将路径u->v的路径看为p,显然,我们可以分解该路径变为u->w->v,其中u->w的路径为p1,w->v的路径为p2,要想使p最短,p1和p2也应该是最短的,所以此问题具有最优子结构。
子问题重叠
适用动态规划方法求解的最优化问题应该具备的第二个性质就是子问题空间必须足够“小”,即问题的递归算法会反复地求解相同的子问题,而不是一直生成新的子问题。如果递归算法反复求解相同的子问题,我们就成最优化问题具有重叠子问题性质。与之相对的,适用于分治方法求解的问题通常在递归的每一步都生成全新的子问题。动态规划算法通常这样利用重叠子问题性质:对每个子问题求解一次,将解存入一个表中,当再次需要这个子问题时直接查表,每次查表的代价为常量时间。
如果问题不具备子问题重叠,即每次产生的都是新的问题,那么动态规划算法中将值存入表中就毫无意义了。
钢条切割问题
问题
给定一段长度为n英寸的钢条和一个价格表p(i)(i = 1, 2, … , n),求切割钢条方案,是的销售收益r(n)最大
自顶向下递归求解
代码如下:
int cut_rod(const vector<int>& price_list, int steel_length)
{
if (steel_length == 0) {
return 0;
}
int max_profit = INT_MIN;
for (int i = 1; i <= steel_length; ++i) {
max_profit = std::max(max_profit,
price_list[i] + cut_rod(price_list, steel_length - i));
}
return max_profit;
}
在实际引用中,效率相当差,它所做的工作量随钢条的长度增大呈指数上升,原因在于它反复求解子问题。
动态规划方法求解
对于通过递归求解,由于它反复求解相同的子问题。因此,动态规划方法仔细安排求解顺序,对每个子问题只求解一次,并将结果保存下来。如果随后再次需要此子问题的解,只需要查找保存的结果,而不必重新计算。因此,动态规划方法是付出额外的内存空间来节省计算时间,是典型的时空权衡的例子
带备忘录的自顶向下法
通过将子问题结果保存在一个数组中从而实现。
// 方法一:带备忘录的自顶向下法
int memorized_cut_rod_aux(const vector<int>&, int, vector<int>&);
int memorized_cut_rod(const vector<int>& price_list, int steel_length)
{
// 备忘录
vector<int> memorandum(steel_length + 1, INT_MIN);
return memorized_cut_rod_aux(price_list, steel_length, memorandum);
}
int memorized_cut_rod_aux(const vector<int>& price_list,
int steel_length,
vector<int>& memorandum)
{
if (memorandum[steel_length] >= 0) {
return memorandum[steel_length];
}
int max_profit = INT_MIN;
if (steel_length == 0) {
max_profit = 0;
}
else {
for (int i = 1; i <= steel_length; ++i) {
max_profit = std::max(max_profit,
price_list[i] + memorized_cut_rod_aux(price_list, steel_length - i, memorandum));
}
}
memorandum[steel_length] = max_profit;
return max_profit;
}
自底向上法
相比较来说,自顶向上法由于没有运用递归,所以它的时间复杂度通常具有更小的系数。
int bottom_up_cut_rod(const vector<int>& price_list, int steel_length)
{
int* memorandum = new int[steel_length + 1];
memorandum[0] = 0;
for (int i = 1; i <= steel_length; ++i) {
int max_profit = INT_MIN;
for (int j = 1; j <= i; ++j) {
max_profit = std::max(max_profit, price_list[j] + memorandum[i - j]);
}
memorandum[i] = max_profit;
}
return memorandum[steel_length];
}
自底向上的动态规划方法处理子问题中的顶点的顺序为:对于一个给定的子问题x,在求解它之前求解邻接至它的子问题y。换句话说就是,对于任何子问题,直至它依赖的所有子问题均已求解完成,才会求解它。在本题目中,要解决长度为4的最优解,只要求解1的时候它的解是最优的,在求解2的时候它的解也是最优的,求解3的时候它的解是最优的,那么求解4的时候他的解也是最优的。
源码
最长公共子序列问题
问题
给定两个序列X=[x1, x2, x3, … , xn]和 Y=[y1, y2, y3, … , yn],求X和Y长度最长的公共子序列
代码
#include <iostream>
#include <utility> // for pair
#include <vector>
using namespace std;
// 动态规划求解最长公共子序列问题
std::pair<vector<vector<int>>, vector<vector<char>>> lcs_length(const string& x_str, const string& y_str)
{
int x_str_size = x_str.size();
int y_str_size = y_str.size();
vector<vector<int>> vv_num(x_str_size + 1); // 用来存放“数字”
vector<vector<char>> vv_ch(x_str_size + 1); // 用来存放“标识”
for (int i = 0; i <= x_str_size; ++i) {
vv_num[i].resize(y_str_size + 1);
vv_ch[i].resize(y_str_size + 1);
}
for (int i = 1; i <= x_str_size; ++i) {
vv_num[i][0] = 0;
vv_ch[i][0] = ' ';
}
for (int j = 0; j <= y_str_size; ++j) {
vv_num[0][j] = 0;
vv_ch[0][j] = ' ';
}
for (int i = 0; i < x_str_size; ++i) {
for (int j = 0; j < y_str_size; ++j) {
if (x_str[i] == y_str[j]) {
vv_num[i + 1][j + 1] = vv_num[i][j] + 1;
vv_ch[i + 1][j + 1] = '1';
}
else if (vv_num[i][j + 1] >= vv_num[i + 1][j]) {
vv_num[i + 1][j + 1] = vv_num[i][j + 1];
vv_ch[i + 1][j + 1] = '2';
}
else {
vv_num[i + 1][j + 1] = vv_num[i + 1][j];
vv_ch[i + 1][j + 1] = '3';
}
}
}
return std::make_pair(vv_num, vv_ch);
}
void print_lcs(const vector<vector<char>>& vv_ch, const string& x_str, int i, int j)
{
if (i == 0 || j == 0) {
return;
}
if (vv_ch[i][j] == '1') {
print_lcs(vv_ch, x_str, i - 1, j - 1);
cout << x_str[i - 1];
}
else if (vv_ch[i][j] == '2') {
print_lcs(vv_ch, x_str, i - 1, j);
}
else {
print_lcs(vv_ch, x_str, i, j - 1);
}
}
int main(int argc, char const *argv[])
{
string x_str, y_str;
cin >> x_str >> y_str;
auto result = lcs_length(x_str, y_str);
/*for (auto i : result.first) {
for (auto j : i) {
cout << j << ' ';
}
cout << endl;
}*/
print_lcs(result.second, x_str, x_str.size(), y_str.size());
return 0;
}
源码
参考
- 《算法导论原书第三版》第十五章动态规划