动态规划(dynamic programming)与分之方法相似,都是通过组合子问题的解来求解原问题。分治方法将问题划分为互不相交的子问题,递归地求解子问题,再将它们的解组合起来,求出原问题的解(如归并排序)。而动态规划应用于子问题重叠的情况,即不同的子问题具有公共的子子问题(子问题的求解是递归进行的,将其划分为更小的子子问题)。在这种情况下,分治算法会做许多不必要的工作,它会反复地求解那些公共子子问题。而动态规划算法对每个子子问题只求解一次,将其解保存在一个表格中,从而无需每次求解一个子子问题时都重新计算,避免了不必要的计算工作。
通常按如下4个步骤来设计一个动态规划算法:
1.刻画一个最优解的结构特征。
2.递归地定义最优解的值。
3.计算最优解的值,通常采用自底向上的方法。
4.利用计算出的信息构造一个最优解。
例子:钢条切割问题
描述:给定一段长度为n英寸的钢条和一个价格表pi(i = 1,2,3...,n)求切割钢条方案,使得销售收益rn最大。
钢条价格表
长度i | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
价格pi | 1 | 5 | 8 | 9 | 10 | 17 | 17 | 20 | 24 | 30 |
如果一个最优解将钢条切割为k段(1≤k≤n),那么最优切割方案 n = i1 + i2 + i3 +...+ ik,得到最大收益 rn = pi1 + pi2 + pi3 + ... + pik。更一般地,对于 rn(n ≥ 1),我们可以用更短的钢条的最优切割收益来描述它:rn = max(pn , r1 + rn-1 , r2 + rn-2, ..., rn-1 + r1),第一个参数pn对应不切割,直接出售长度为n的钢条的方案。其他n-1个参数对应另外n-1种方案:对每个i = 1,2,...,n-1,首先将钢条切割为长度为i 和 n -i 的两段,接着求解这两段的最优切割收益ri 和rn-i。由于无法预知哪种方案会获得最优收益,我们必须考察所有可能的i,选取其中收益最大者。
注意到,为求解规模为n的原问题,我们先求解形式完全一样,但规模更小的子问题。即当完成首次切割后,我们将两段钢条看成两个独立的钢条切割问题实例。通过组合两个关于子问题的最优解,并在所有可能的切割方案中选取组合收益最大者,构成原问题的最优解。动态规划方法仔细安排求解顺序,对每个子问题只求解一次,并将结果保存下来。是付出额外的内存空间来节省计算时间。有两种等价的实现方法:
一,带备忘的自顶向下法。此方法按自然的递归形式编写过程,但过程会保存每个子问题的解,当需要一个子问题的解时,过程首先检查是否已经保存过此解。如果是,则直接返回保存的值,从而节省了计算时间;否则,按通常方式计算这个子问题。
二,自底向上法。这种方法一般需要恰当定义子问题"规模"的概念,使得任何子问题的求解只依赖于"更小"的子问题的求解。因而可以将子问题按规模排序,按由小到大的顺序进行求解。当求解某个子问题时,它所依赖的那些更小的子问题都已求解完毕,结果已经保存。
自底向上法代码如下:
1 void bottomUpCutRod(const vector<int> &p,int n , vector<int> &r) 2 { 3 r.resize(n+1); 4 r[0] = 0; 5 int q = -1; 6 7 for(int j = 1; j <= n; ++j){ 8 q = -1; 9 for(int i = 1; i <= j; ++i){ 10 q = std::max(q,p[i] + r[j-i]); 11 } 12 r[j] = q; 13 } 14 }
例子:
1 int main() 2 { 3 int p[] = {-1,1,5,8,9,10,17,17,20,24,30}; 4 vector<int> vp(p,p + sizeof(p)/sizeof(int)); 5 vector<int> result; 6 bottomUpCutRod(vp,9,result); 7 for(int i = 1; i <=9; ++i) 8 cout << result[i] << " "; 9 }
结果保存在result数组中,长度为i的钢条切割的最优收益值为result[i]。
输出: