给定一个问题, 能够使用动态规划的方法解决的一个标志就是, 这个问题具有最优子结构(optimal substructure)。 我们可以认为, 能够使用DP算法的问题首先能够分解为子问题。 而且我们可以利用子问题的最优解去解决原始问题的解。 这当然需要我们在解决了子问题的时候, 设法对子问题的最优解存储起来。这就是所谓的DP ≈ recursion(递归) + memoirizaion(备忘)。
下面我们通过算法导论书上的cutting a rod 的例子来说明如何去使用DP算法。
一个rod 的长度是n, 对于不同长度的rod, 价格不同。 用Pi 代表长度为i 的rod 的价格(i = 1, 2, 3, ... n )。 价格表如下:
下在问题来了:
当给我们个长度为4 的 rod, 让我们选择一种切割方案, 使得我们得到的总的收益是最大的???
如上图, 箭头表示我们的选择, 要么切割, 要么不切两种可能。 所以总共有2^(4 - 1) = 8 种方案。 考虑到由于对称性导致的重复, 其实只有五(4 + 1)种方案, 如下:
方案 收益
(4, 0) 9
(1, 3) 9
(2, 2) 10
(1, 1, 2) 7
(1, 1, 1, 1,) 4
显然, 上述最佳的切割方案是(2, 2), 收益为10。
上述采用的是brute-force 算法, 显然时间复杂度能够到达O(2^(n-1)), 到达指数了, 显然效果很差, 算法伪代码如下:
解释, 进入循环:
i = 1, 表示在第一个长度单位的rod 是否切下来做决策, q = max(q, p[1] + CUT-ROD(p, n - 1)), 接下来就涉及到对于i = 1 的递归调用。
i = 2, 同理, q = max(q, p[2] + CUT-ROD(p, n - 2)), 意思是第一次切割位于距离头为单位2的距离, 接下来, 对右边的那段进行递归调用。
............
i = n, 这种情况表示不切割, 还需要比较前面所有的切割方案中收益最大的值(存储在q)中, 然后和 不切割的方案比较, 去最大值就是我们的最大的收益。
上述算法达到exponential asymptotic time。
所以为了将算法的时间复杂度降下来, 我们使用dynamic programming:
这个问题为什么能用DP算法呢???
首先, 因为这个问题具有optimal substructure。 举个例子, 假设我们计算第一次切割在(3, 1)的最佳切割方案, 此时3 可以继续切割下去, 但是如果我们已经知道3 的最佳切割方案的收益的话(当然, 解决完子问题的时候, 需要将解存储下来), 我们直接采用3的最佳值直接加上1 即可以得到(3, 1)的后序最佳切割方案得到的收益值了。 这就是所谓的optimal substructure。 而且能够通过recursive 求解。
所以, 有如下注意的地方:
记录B(i) 为长度为i 的rod 采用最佳切割得到的收益。 所谓的最佳切割就是使得收益达到最大的切割。
那么, 我们有如下公式(Vk 代表Pk):
例如, 对于L = 8 的rod , 最大值B(8)的计算如下:
(1, 7), (2, 6), (3, 5), (4, 4), (5, 3), (6, 2), (7, 1), (8, 0)中得到。 pair 对应的第二个为需要求解的最佳值。
所以计算方法如下。
dynianc programming 的 bottom-up 形式的算法如下:
每当计算一个自问题的最佳解, 我们存储到表格中:
依次类推下去, 最终我们到达B(8), 如下:
如下:
这样, 我们就解决了这个问题。
整个算法的步骤如下:
这样动态规划将时间复杂度从exponential time 降低到了polynomial time
算法的伪代码为:
程序如下:
#include <iostream>
#include <cstring>
using namespace std;
const int N = 1000;
int p[11];
int r[N], s[N];
//initializer for prices and optimal solution
void init() {
memset(r, -1, sizeof(r));
r[0] = 0;
p[0] = 0;
p[1] = 1;
p[2] = 5;
p[3] = 8;
p[4] = 9;
p[5] = 10;
p[6] = 17;
p[7] = 17;
p[8] = 20;
p[9] = 24;
p[10] = 30;
}
//naive exponential solution
int cutRod(int n) {
int q = 0;
for(int i = 1; i <= n; i++) {
q = max(q, p[i] + cutRod(n - i));
}
return q;
}
//top-down solution
int topDownCutRod(int n) {
if(r[n] != -1)
return r[n];
int q = 0;
for(int i = 0; i <= n; i++) {
q = max(q, p[i] + topDownCutRod(n - i));
}
return r[n];
}
//bottom-up solution
int bottomUpCutRod(int n) {
if(r[n] != -1)
return r[n];
for(int j = 0; j <= n; j++) {
int q = 0;
for(int i = 1; i <= j; i++) {
q = max(q, p[i] + r[j - 1]);
}
r[j] = q;
}
return r[n];
}
//bottom-up solution that maintains not only the best
//price but also the "required cut" for such solution
int extendedBottomUpCutRod(int n) {
if(r[n] != -1)
return r[n];
for(int j = 1; j <= n; j++) {
int q = 0;
for(int i = 1; i <= j; i++) {
if (q < p[i] + r[j - i]) {
q = p[i] + r[j - i];
s[j] = i;
}
}
r[j] = q;
}
return r[n];
}
//print the extended method's output
void printCutRodSol(int n) {
do {
cout << s[n] << " ";
} while((n -= s[n]) > 0);
}
int main() {
init();
int n;
cout << "please input the length of the rod: " << endl;
cin >> n;
cout << endl;
// cout << cutRod(4) << endl;
// cout << cutRod(4) << endl;
cout << extendedBottomUpCutRod(n) << endl;
printCutRodSol(n);
return 0;
}
运行结果如下: