以下是我自己对于动态规划的一点点理解,如果有什么偏差之处还望各位大佬能够指点一二。
【注】以下例子均为理想状态下,不要考虑各种意外的可能性,谢谢!
来点感性的认识
下面我们先对动态规划有一个感性的认识。
动态规划的本质是寻找最优解,也就是说,对于一个问题,它可能会存在很多个解,但是其中有一种方法是这些解中最好的。
比如说,我从武汉到深圳,我可以坐绿皮火车,坐动车,坐飞机,甚至是坐大巴,我们现在要找到一种最快到深圳的方式,那毫无疑问是坐飞机。这就是最优解。
当然,从宏观角度来看,动态规划就是一个寻找最优解的过程。如果我们的动态规划就如上述例子一样简单,那咱们还搞啥,不就是选出最快的交通工具就行了?但事实并非如此,我们需要将一个大的问题分解成一些列小的问题,并求解出这些小问题,也就是子问题的最优解,你想想,当所有的子问题都是最优解的时候那么原问题是不是也是最优解。
还是用上面的例子举例,我现在要从武汉的某个具体位置A到达深圳的某个具体位置B。那么我肯定不可能推开门就可以坐飞机动车对不对,我需要先乘坐其他的交通工具到达火车站或者飞机场或者客运站,然后才能选择长途交通工具,到了深圳之后,我还得乘坐短途交通工具到达B才行。那么我们要求解的就是我从A到B选择哪几种交通工具是最快的。此时的大问题就分成了三段,1)乘坐啥A到武汉长途交通工具站点最快;2)乘坐啥从武汉长途交通工具站点到深圳长途交通工具站点最快;3)乘坐啥从深圳的长途交通工具站点到B最快。对于情况1)我们也有多种选择方式:坐公交车,坐地铁,打的等。那么就从这些中选择一个能最快到达武汉长途交通工具站点的方式,同样的,到了深圳之后,也可以选择最快的方式到达B,那么这三段中都选出了最快的交通方式,结果不就是A到B最快的方法吗?
那么这就引出了动态规划中两个必不可少的两个要素:子问题最优解和子问题重叠。
根据上面的例子,我们已经了解到什么是子问题最优解,就是1),2),3)的最优解,1),2),3)就是三个子问题。那么我们现在来看看什么是子问题重叠。
子问题重叠也很好理解,还是用上面的例子,我们得通过计算才能知道1),2),3)中的各个交通工具到达目的地的所用时长是多少,如果我们不对前一段路程中各个交通工具所用时长进行保存的话,那么在下一段中我们需要重复计算上一段的工具所用时长。这就是子问题重复。也就是说,一个子问题可能会再其他子问题中被反复计算。
如下图所示:A到武汉长途交通枢纽站有三种方式:公交1h,地铁30min,打的25min;武汉到深圳有四种方式:火车15h,地铁5h,飞机2h,长途汽车18h;深圳长途交通枢纽站到B也有三种方式:公交2h,地铁50min,打的1h。那么我们从A到B就有好几种可行解{公交,火车,公交}、{公交,高铁,地铁}、{地铁,飞机,地铁}等等。那么我们要从这么多可行解中找出最优解,也就是最快A到B的解。我们可以发现是{打的,飞机,地铁}。
而我们要求出最优解,就必须列出所有可行解,然后一一比较得出结论。那么在求解过程中会出现重复求解的情况,也就是子问题重叠。比如说我2)中选择了火车,那么我就得求出1)中最快的,经过比较发现是打的;然后2)中我再选择高铁时,还得再一次求解一遍1)中最快的交通工具。这就出现了重复求解的问题(你要知道,计算机可不会自动帮你记住你之前求解过的结果,虽然咱们一看就知道1)中选择打的最快,可是计算机不知道啊!所以你必须采用某种方式让计算机知道1)中打的是最快的,那么在2)中我想判断最优解的时候直接加上1)中的打的时间就ok了)。
经过一系列的比较判断最优解求解之后我们得出了结论,也就是用荧光笔标记的路线,以这种方式来走是最快的。
对于子问题会被反复计算这个问题,我们当然是不希望看到的,明明之前已经计算过的东西,还要浪费时间再算一遍真的好气哦!所以聪明的大佬们就发明了两种方法来解决这个问题,让计算机这个看似聪明实际上不太聪明的家伙来记住之前算过的结果。这两个方法分别是:自顶向下的备忘录和自底向上的方法。其实这两方法原理是一样的,只不过一个用迭代一个用递归。其本质都是将之前算过的结果用一个数组记录下来(这就有点像是一个表格状的参考答案),下回再要用的时候就查一下表,找找有没有这个答案,有的话咱们就不用再算一遍了,直接用这个答案就ok啦!
来个常用例子感受感受
最长公共子序列
问题描述: 有两个序列X和Y,Xm = {x1,x2,…,xm},Yn = {y1,y2,…,yn},它们的最长公共子序列长度为k,该子序列不需要连续。例如:X6 = {A, B, C, C, B, A},Y8 = {A, C, C, A, B, C, A, B},最长公共子序列为{A, C, C, B, A},长度为5.
问题求解:
- 当xm == yn时,求解Xm-1和Yn-1的最长公共子序列 + 1。(即求解 {x1,x2,…,xm-1}和{y1,y2,…,yn-1}的最长公共子序列,+1表示已经加入了序列中的一个元素)
- 当xm != yn时,①求解Xm-1和Yn的最长公共子序列;②求解Xm和Yn-1的最长公共子序列;③求解max{①,②}
【温馨提示】这里时倒着比较的,其实顺着比较也是阔以的。
根据上面的问题求解方法,我们可以写出状态方程:
(c[i, j]表示遍历到 xi 和 yj 时的最长公共子序列的长度。)
代码
#include <iostream>
#include <vector>
#include <Windows.h>
using namespace std;
template<class T>
int LCS(vector<T>, vector<T>);
int main() {
vector<char> X{ 'A', 'B', 'C', 'C', 'B', 'A' };
vector<char> Y{ 'A', 'C', 'C', 'A', 'B', 'C', 'A', 'B' };
cout << LCS(X, Y) << endl; // 5
vector<int> a{ 1,2,3,3,2,2 };
vector<int> b{ 2,3,2,1,2,2 };
cout << LCS(a, b) << endl; // 4
return 0;
}
// 最长公共子序列
template<class T>
int LCS(vector<T> X, vector<T> Y) {
/// X[1~m] Y[1~n] c[0~m][0~n]
if (X.empty() || Y.empty()) return 0;
int res = INT_MIN; // 保存LCS的最长长度
// 为了让X和Y从1开始计数,在最前面插入一个无关数
X.insert(X.begin(), T(-1));
Y.insert(Y.begin(), T(-1));
const int m = X.size();
const int n = Y.size();
vector<vector<int>> c(m, vector<int>(n)); // m * n
// 初始化
for (int i = 0; i < m; i++)
c[i][0] = 0;
for (int j = 1; j < n; j++)
c[0][j] = 0;
// 计算最大值
for(int i = 1; i < m; i++)
for (int j = 1; j < n; j++) {
if (X[i] == Y[j]) c[i][j] = c[i - 1][j - 1] + 1;
else
c[i][j] = max(c[i - 1][j], c[i][j - 1]);
res = max(res, c[i][j]);
}
return res;
}
reference
[1] 《算法导论》chapter15