动态规划和分治策略相似,不同的是,它针对的问题所分解出的小问题数量很多且很多是重复的。动态规划就是使得这些重复的小问题只需要计算一次,避免重复计算。
钢条切割问题:给定一段长度为n英寸的钢条和一个价格表pi(i=1,2,...,n)求切割钢条方案,使得销售收益rn最大。
钢条切割问题:给定一段长度为n英寸的钢条和一个价格表pi(i=1,2,...,n)求切割钢条方案,使得销售收益rn最大。
注意,如果长度为n英寸的钢条的价格pn足够大,最优解可能就是完全不需要切割(具体问题描述见算法导论第三版第15章),以下都是用C语言实现,伪代码参考算法导论。
第一次尝试:自顶向下的递归算法
<span style="font-size:18px;">#include <stdio.h>
#include "limits.h"
#define INT_MIN (-INT_MAX - 1)
int CutRod(int *p,int n);
void main()
{
int p[]={0,1,5,8,9,10,17,17,20,24,30};
printf("%d\n",CutRod(p,10));
}
int CutRod(int *p,int n)
{
if(n==0)
return 0;
int q=INT_MIN;
for (int i=0;i<n;i++)
{
int tmp=CutRod(p,n-1-i)+p[i];
q=(q>tmp?q:tmp);
}
return (q);
}</span>
上面只用了分治策略,这个算法的性能是很差的,为T(n)=2^n,这种朴素递归算法效率之所如此底是因为在子问题的求解中很多都是重复的。
第二次尝试:备忘录与自底向上
动态规划方法是仔细安排求解顺序,对每个子问题只求解一次,并将结果保存下来(其实是付出额外的内存空间来节省运行时间,是典型的时空权衡)。
动态规划的两种方法——带备忘录的自顶向下法和自底向上法,首先介绍下带备忘录的自顶向下法:
备忘录方法是动态规划方法的变形。与自底向上的动态规划算法不同的是,备忘录方法的递归方式是自顶向下的,此方法仍然按照递归的形式,但过程中会保存每个子问题的解。当需要一个子问题的解时,首先检查是否已经保存过此解。如果是,则直接返回保存的解,从而节省时间。
C实现:
<span style="font-size:18px;">#include <stdio.h>
#include "limits.h"
#define INT_MIN (-INT_MAX - 1)
int Memorize_Cut_Rod_Aux(int *p,int n,int *r);
int Memorize_Cut_Rod(int *p,int n);
void main()
{
int p[]={0,1,5,8,9,10,17,17,20,24,30};
printf("%d\n",Memorize_Cut_Rod(p,10));
}
int Memorize_Cut_Rod_Aux(int *p,int n,int *r)
{
if(n==0)
return 0; //这个地方要注意,和书上做了点调整
if(*(r+n-1)>0)
return (*(r+n-1));
int q=0;
for (int i=1;i<n+1;i++)
{
int tmp=Memorize_Cut_Rod_Aux(p,n-1-i,r)+p[i];
q=(q>tmp?q:tmp);
}
*(r+n-1)=q;
return (q);
}
int Memorize_Cut_Rod(int *p,int n)
{
int r[10];
for (int i=0;i<n;i++)
*(r+i)=INT_MIN;
return (Memorize_Cut_Rod_Aux(p,n,r));
}</span>
自底向上法——这种方法一般需要恰当定义子问题“规模”的概念,使得任何子问题的求解都依赖于“更小的”子问题的求解。因而我们可以将子问题按规模排序,按由小至大的顺序进行求解。当求解某个子问题时,它所依赖的那些更小的子问题都已经求解完毕,结果已经保存。每个子问题只需要求解一次,当我们求解它(也是第一次遇到它)时,它的所有前提子问题都已求解完成。
C实现:
<pre name="code" class="cpp"><span style="font-size:18px;">#include <stdio.h>
#include <stdlib.h>
#include "limits.h"
#define INT_MIN (-INT_MAX - 1)
void main()
{
int p[]={0,1,5,8,9,10,17,17,20,24,30};
int r[11]={0};
for (int j=1;j<11;j++)
{
int q=INT_MIN;
for (int i=1;i<j+1;i++)
{
int tmp=p[i]+r[j-i];
q=(q>tmp?q:tmp);
}
r[j]=q;
}
for(int i=1;i<11;i++)
printf("%d\n",r[i]);
}</span>
下面是上面的扩展版本,还保存了最优解对应的第一段钢条的切割长度Sj;
<span style="font-size:18px;">#include <stdio.h>
#include <stdlib.h>
#include "limits.h"
#define INT_MIN (-INT_MAX - 1)
void Extended_Bottom_Up_Cut_Rod(int *p,int n,int *r);
void main()
{
int p[]={0,1,5,8,9,10,17,17,20,24,30};
int r[11]={0};
int s[11]={0};
int n=10,q=INT_MIN;
for (int j=1;j<=n;j++)
{
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;
}
for(int i=1;i<11;i++)
printf("%d,%d\n",r[i],s[i]);
}
void Extended_Bottom_Up_Cut_Rod(int *p,int n,int *r)
{
for(int i=0;i<2*n;i++)
r[i]=0;
for (int j=0;j<n;j++)
{
int q=INT_MIN;
for (int i=0;i<j-1;i++)
{
if(q<p[i]+r[j-i-1])
{
q=p[i]+r[j-i-1];
r[n+j-1]=i;
}
}
r[j]=q;
}
}</span>