在《算法导论》动态规划那章,讲述了一个应用动态规划的例子--钢条切割。
钢条切割的问题是这样的:给定一段长度为n英寸的钢条和一个价格表(i=1,2,...,10),求切割钢条方案,使得销售收益最大。
我们可以采用一种简单的递归求解方法:我们将钢条从左边切割下长度为i的一段,只对右边剩下的 长度为n-i的一段继续进行切割(递归求解),对左边的一段则不再进行切割。用公式可以表示为:
方法一:带备忘的自顶向下法。
此方法按自然的递归形式编写过程,但过程会保存每个子问题的解,这里我们保存在数组r中。当需要一个子问题的解时,过程首先检查是否已经保存过此解。如果是,则直接返回保存的值,从而节省计算时间;否则,按通常形式计算这个子问题。我们称这个递归过程是带备忘的(memoized),因为它“记住”了之前已经计算的结果。代码如下:
#include<iostream>
using namespace std;
//对应于长度为1,2...10的钢条价格表
int p[10]={1,5,8,9,10,17,17,20,24,30};
//返回两个数的较大值
int max(int a,int b)
{
return (a>b)?a:b;
}
/*首先检查所需值是否已知,如果是,则直接返回保存的值。
否则,用通常方法计算所需值q,然后将此值存入r[n]。值得注意的
是,笔者在此处没有对钢条的长度n进行限制,所以n可能大于10,
于是增加了自己的判断,若n大于10,则计算n-10的子序列*/
int MemoizedCutRodAux(int p[],int n,int r[])
{
if(r[n]>=0)
return r[n];
int q;
if(n==0)
q=0;
else
{
q=-1;
for(int i=0;i<n;i++)
{
if(i<10)
q=max(q,p[i]+MemoizedCutRodAux(p,n-1-i,r));
else
q=max(q,r[10]+MemoizedCutRodAux(p,n-1-10,r));
}
}
r[n]=q;
return q;
}
int main()
{
int n;
cout<<"请输入钢条的长度:";
cin>>n;
//将辅助数组的r[0..n]元素均初始化为-1
int *r=new int[n+1];
for(int i=0;i<=n;i++)
{
r[i]=-1;
}
int sum=MemoizedCutRodAux(p,n,r);
cout<<"最大收益为:"<<sum<<endl;
return 0;
}
运行结果:
方法二:自底向上法
这种方法一般需要恰当定义子问题规模的概念,使得任何子问题的求解都只依赖于“更小的”子问题的求解。因而我们可以将子问题按规模排序,按由小至大的顺序进行求解。当求解某个子问题时,它所依赖的那些更小的子问题都已求解完毕,结果已经保存。每个子问题只需求解一次,当我们求解它时,它的所有前提子问题都已经求解完成。
#include<iostream>
using namespace std;
//对应于长度为1,2...10的钢条价格表
int p[10]={1,5,8,9,10,17,17,20,24,30};
//返回两个数的较大值
int max(int a,int b)
{
return (a>b)?a:b;
}
/*首先创建一个新数组r来保存子问题的解,然后将r[0]初始化为0,因为长度为0的钢条没有收益
接着对j=1,2,...,n按升序求解每个规模为j的子问题。将规模为j的子问题的解存入r[j]。最后返回r[n],即最优解*/
int BottomUpCutRod(int p[],int n)
{
int *r=new int[n+1];
r[0]=0;
for(int j=1;j<=n;j++)
{
int q=-1;
for(int i=0;i<=j;i++)
{
if(i<10)
q=max(q,p[i]+r[j-i-1]);
else
q=max(q,r[10]+r[j-10-1]);
}
r[j]=q;
}
return r[n];
}
int main()
{
int n;
cout<<"请输入钢条的长度:";
cin>>n;
int sum=BottomUpCutRod(p,n);
cout<<"最大收益为:"<<sum<<endl;
return 0;
}
运行结果为: