动态规划

动态规划与分治法的区别

动态规划与分治法相似,都是通过组合子问题的解来求解原问题。

分治法将问题划分为互不相交的子问题,递归地求解子问题,再将它们的解组合起来,求出原问题的解。

与之相反,动态规划应用于子问题重叠的情况。这种情况下,分治算法会反复地求解相同的子问题,而动态规划算法会将这些重复子问题的解保存起来(通常使用数组),避免不必要的计算工作。

 

动态规划算法的步骤

我们通常按如下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 }
View Code

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 }
View Code

 

重构解

上面给出了使用动态规划方法求解钢条切割问题的最优值。

修改我们的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 }
View Code

 

posted @ 2015-07-28 17:24  Runnyu  阅读(446)  评论(0编辑  收藏  举报