动态规划
动态规划与分治法的区别
动态规划与分治法相似,都是通过组合子问题的解来求解原问题。
分治法将问题划分为互不相交的子问题,递归地求解子问题,再将它们的解组合起来,求出原问题的解。
与之相反,动态规划应用于子问题重叠的情况。这种情况下,分治算法会反复地求解相同的子问题,而动态规划算法会将这些重复子问题的解保存起来(通常使用数组),避免不必要的计算工作。
动态规划算法的步骤
我们通常按如下4个步骤设计一个动态规划算法(最优解跟最优值是不同的概念,下面会举例讲到):
1.刻画一个最优解的结构特征
2.递归地定义最优值
3.计算最优值,通常采用自底向上的方法
4.利用计算出的信息构造出一个最优解
钢条切割问题
钢条切割问题是这样的:给定一段长度为n英寸的钢条和一张价格表(如下图所示),求切割钢条方案(最优解),使得销售利益(最优值)最大。
长度为n英寸的钢条共有2^n-1种切割方案,因为每一英寸都可以选择切割或不切割。
如果一个最优解将钢条切割成k端(1≤k≤n)
那么最优切割方案:
得到的最大利益为:
对于rn(n≥1),我们可以用更短的钢条的最优切割利益来描述它(rk(1≤k≤n)代表长度为k英寸的钢条切割之后的最大利益)
为了求解规模为n的原问题,我们先求解形式完全一样,但规模更小的子问题:
在首次切割后,我们将两端钢条看成两个独立的钢条切割问题的实例。
我们通过组合两个相关子问题的最优解,并在所有可能的两段切割方案中选取组合收益最大者,构成原问题的最优解。
我们称钢条切割问题满足最优子结构性质:问题的最优解由相关子问题的最优解组合而成,而这些子问题可以独立求解。
我们可以使用简单递归的方法来求解这个问题:
将长度为n的钢条分解为左面开始一段,以及剩余的一段(继续分解)。于是我们可以得到下面的公式:
下面过程实现了该公式的计算,它采用的是一种直接的自顶而下的递归方法
CUT-ROD(p,n) if n==0 return 0 q=-∞ for i=1 to n q=max(q,p[i]+CUT-ROD(p,n-i)) return q
过程CUT-ROD以价格数组p[1...n]和整数n位输入,返回长度为n的钢条的最大利益。
CUT-ROD的效率很差,因为CUT-ROD反复地用相同的参数值对自身进行递归调用,即反复求解相同的子问题。
使用动态规划方法求解钢条切割问题:
动态规划对于每个子问题只求解一次,并将结果保存下来。如果随后再次需要此子问题的解,只需查找保存的结果而不必重新计算。
动态规划有两种等价的实现方法,下面以钢条切割问题为例展示这两种方法:
1.带备忘的自顶向下的方法
下面给出的是自顶向下CUT-ROD过程的伪代码,加入了备忘机制:
//初始化 MEMOIZED-CUT-ROD(p,n) let r[0..n] be a new arrar for i=0 to n r[i]=-∞ return MEMOIZED-CUT-ROD-AUX(p,n,r) //算法主体 MEMOIZED-CUT-ROD-AUX(p,n,r) if r[n]≥0 return r[n] if n==0 q=0 else q=-∞ for i=1 to n q=max(q,p[i]+MEMOIZED-CUT-ROD-AUX(p,n-1,r)) r[n]=q return q
每个子问题的解保存在数组r中,其中每个元素初始化为负无穷来表示未知值。
调用MEMOIZED-CUT-ROD-AUX第一步判断所需值是否已知,如果是,则直接返回保存的值,避免了重复计算子问题。
自顶向下的方法可以这样理解:每次的递归调用会将问题分解为子问题,可以从最终的子问题开始着手,本例可以从MEMOIZED-CUT-ROD-AUX(p,0,r)尝试往上推。
下面是该方法的实现代码
1 #define N 10 2 #include <iostream> 3 #include <algorithm> 4 using namespace std; 5 6 //初始化在main函数中实现 7 int memoized_cut_rod(int p[],int n,int r[]) 8 { 9 if(r[n]>=0) 10 return r[n]; 11 int q; 12 if(n==0) 13 q=0; 14 else 15 { 16 q=-1; 17 for(int i=1;i<=n;++i) 18 q=max(q,p[i]+memoized_cut_rod(p,n-i,r)); 19 } 20 r[n]=q; 21 return q; 22 } 23 24 int main() 25 { 26 //数组p从1开始,因此把p[0]设置为0 27 int p[]={0,1,5,8,9,10,17,17,20,24,30}; 28 int r[N+1]; 29 for(int i=0;i<=N;++i) 30 r[i]=-1; 31 for(int i=1;i<=N;++i) 32 cout<<"n="<<i<<" r="<<memoized_cut_rod(p,i,r)<<endl; 33 return 0; 34 }
2.自底向上法
BOTTOM-UP-CUT-ROD(p,n) let r[0..n] be a new array r[0]=0 for j=1 to n q=-∞ for i=1 to j q=max(q,p[i]+r[j-i]) r[j]=q return r[n]
自底向上版本更为简单,先创建一个数组r来保存子问题的解。对j=1,2,...,n按升序求解每个规模为j的子问题。
自底向上的方法似乎更容易理解:钢条长度从1开始每次递增并记录当前长度的最优值。
下面是该方法的实现代码。为了打印最优值,数组r的初始化放在了main函数中,然后作为参数传进botton_up_cut_rod函数中
1 #define N 10 2 #include <iostream> 3 #include <algorithm> 4 5 using namespace std; 6 7 int botton_up_cut_rod(int p[],int n) 8 { 9 int r[N+1]; 10 r[0]=0; 11 int q; 12 for(int j=1;j<=n;++j) 13 { 14 q=-1; 15 for(int i=1;i<=j;++i) 16 q=max(q,p[i]+r[j-i]); 17 r[j]=q; 18 cout<<"n="<<j<<" r="<<r[j]<<endl; 19 } 20 return q; 21 } 22 23 int main() 24 { 25 int p[]={0,1,5,8,9,10,17,17,20,24,30}; 26 botton_up_cut_rod(p,N); 27 return 0; 28 }
重构解
上面给出了使用动态规划方法求解钢条切割问题的最优值。
修改我们的BOTTON-UP-CUT-ROD方法,让它保存最优解的信息(数组s),根据这些信息来构造最优解。
EXTENDED-BOTTOM-UP-CUT-ROD(p,n) let r[0..n] and s[0..n] be new arrays r[0]=0 for j=1 to n q=-∞ for i=1 to j if q<p[i]+r[j-i] q=p[i]+r[j-i] s[j]=i r[j]=q return r and s
跟BOTTON-UP-CUT-ROD有一点不同的是:
在求解规模为j的子问题时将第一段钢条的最优切割长度i保存在s[j]中。EXTENDED-BOTTOM-UP-CUT-ROD(p,10)会返回下面的数组
s[1..n]记录了每条钢条的长度,可以根据数组s构造最优解。
下面是实现代码。为了输出最优值跟最优解,数组r跟数组s的定义都放在main函数中。
1 #define N 10 2 #include <iostream> 3 4 using namespace std; 5 6 int extened_botton_up_cut_rod(int p[],int r[],int s[],int n) 7 { 8 int q; 9 for(int j=1;j<=n;++j) 10 { 11 q=-1; 12 for(int i=1;i<=j;++i) 13 { 14 if(q<p[i]+r[j-i]) 15 { 16 q=p[i]+r[j-i]; 17 s[j]=i; 18 } 19 } 20 r[j]=q; 21 } 22 return q; 23 } 24 25 int main() 26 { 27 int p[]={0,1,5,8,9,10,17,17,20,24,30}; 28 int r[N+1]={0}; 29 int s[N+1]={0}; 30 extened_botton_up_cut_rod(p,r,s,N); 31 for(int i=1;i<=N;++i) 32 { 33 cout<<"n="<<i<<" r="<<r[i]<<" "; 34 //构造最优解 35 cout<<"solution:"; 36 int n=i; 37 while(n>0) 38 { 39 cout<<s[n]<<" "; 40 n=n-s[n]; 41 } 42 cout<<endl; 43 } 44 return 0; 45 }