问题引入:
某公司出售的长为i的钢条的价格为p(i)(单位为元),如下表所示。如果有一段长为n的钢条,求如何切割才能使收益最大?
测试的最优方案:
1.最容易想到的解法
思路:我们把长度为n的钢条分为i和n-i两部分,1<=i<=10,且不需要再分割,则只需要对n-i求最大收益相加即可。同理对n-i也分为两段,左边的不需要分解,继续分解右边的,直到右边的部分为0。这是一种自顶向下的递归方法。具体代码如下:
public class Guihua {
public int cutProfit(int len) {
int sum=0;
int[] price= {0,1,5,8,9,10,17,17,20,24,30};//长度价格表
if(len==0)
return 0;
for(int i=1;i<=len && i<price.length;i++)
{
int tmpsum=price[i]+cutProfit(len-i);//将len长度的钢条切割成两段,递归求解第二段
if(tmpsum>sum)
sum=tmpsum;
}
return sum;
}
public static void main(String[] args)
{
long startTime=System.currentTimeMillis();
System.out.println(new Guihua().cutProfit(20));
long endTime=System.currentTimeMillis(); //获取结束时间
System.out.println("程序运行时间: "+(endTime-startTime)+"ms");
}
}
运行上述代码,结果与上标一致。但是运行时间很慢,n=20时的运行结果长达363ms。n=50,超时,没有运行出结果。代码之所以效率很低,是应为上述递归方法反复求解相同的子问题。如n=5,需要求n=3,;n=4,又需要求n=3。这时就需要用动态规划算法了。
2.动态规划算法
对上述问题,运用动态对话算法仔细安排求解顺序,对每个子问题只需要求解一次,并将结果保存下来,下次用到该结果时,只需要直接读取,不需要重复计算。因此动态规划方法时付出额外的内存空间来节省计算时间,时时空权衡的例子。
动态规划有两种等价的实现方法,分别是
带备忘录的自顶向下法和
自底向上法,以钢条切割问题展示如下:
2.1带备忘录的自顶向下法
第一种为
带备忘录的自顶向下法。此方法仍用递归实现,但是过程会保存子问题 的解(可以保存在数组中)。需要子问题的解时,先检查是否保存该值,如保存直接用;否则,按上面的方法计算。这其实是一种不断分解的方法。在Java实现中,meArray存储计算好的最优子问题的解,由于在递归程序中初始化数组会破坏已经赋值的数组元素,所以将该数组放置函数外面初始化,并用函数调用。
带备忘录的自顶向下法实现:
public class Guihua {
int fromTop(int len,int[] meArray)
{
int sum=0;
int[] price= {0,1,5,8,9,10,17,17,20,24,30};//长度价格表
if(len==0)
return 0;
if(meArray[len]!=0) //已存,直接使用
return meArray[len];
for(int i=1;i<=len && i<price.length;i++)//前半段只能是1到10,没有大于10的规格的钢条
{
int tmpsum=price[i]+fromTop(len-i,meArray);
if(tmpsum>sum)
sum=tmpsum;
}
meArray[len]=sum;
return sum;
}
public static void main(String[] args)
{
long startTime=System.currentTimeMillis();
int len=20;
int[] meArray=new int[len+1];//记忆数组,记录已经算出的值
System.out.println( new Main().fromTop(len, meArray));
for(int ele:meArray)
System.out.print(ele+" ");
long endTime=System.currentTimeMillis(); //获取结束时间
System.out.println("程序运行时间: "+(endTime-startTime)+"ms");
}
}
查看运行结果发现n=20,用时3ms,输出的记忆数组为:0 1 5 8 10 13 17 18 22 25 30 31 35 38 40 43 47 48 52 55 60,程序效率提高。但是程序中没有给出最优解得具体形式。下面给出方法:
2.1.1含有最优方案的带备忘录的自顶向下法
由上一阶段,我们得到了对不同n对应的最大收益。第一段长度先固定,求第二段的分解方案,如果我们比较出一个较大的第二段长度,就把对应的长度存起来,这样最后存的肯定是收益最大时的其中一段长度,我们把它称作确定段长度。由总长度和确定段长度,二者相减就可以确定下一段长度;然后由下一段长度和它对应的确定长度又可确定下下段长度,然后依次运算,直到相减等于0。可以参考下面的表格:
具体实现仍采用递归的方法:
public class Guihua {
int fromTopDetail(int len, int[] meArray, int[] detailArray) {
int sum=0;
int[] price= {0,1,5,8,9,10,17,17,20,24,30};//长度价格表
if(len==0)
return 0;
if(meArray[len]!=0) //
return meArray[len];
for(int i=1;i<=len && i<price.length;i++)
{
int tmpsum=price[i]+fromTopDetail(len-i,meArray,detailArray);
if(tmpsum>sum)
{
sum=tmpsum;
detailArray[len]=i;
}
}
meArray[len]=sum;
return sum;
}
void getDeatil(int[] methodArray,int index) { //由第一段长度,求解优方案
if(methodArray[index]==index)
{
System.out.println(methodArray[index]);
return;
}
else
System.out.println(methodArray[index]);
getDeatil(methodArray,index-methodArray[index]);
}
public static void main(String[] args)
{
long startTime=System.currentTimeMillis();
int len=9;
int[] meArray=new int[len+1];//记忆数组,记录已经算出的值
int[] detailArray=new int[len+1];//记忆数组,记录已经算出的值
System.out.println( new Main().fromTopDetail(len, meArray, detailArray));
new Main().getDeatil(detailArray,detailArray.length-1);
long endTime=System.currentTimeMillis(); //获取结束时间
System.out.println("程序运行时间: "+(endTime-startTime)+"ms");
}
}
2.2含具体方案的自底向上法
第二种方法称为自底向上法。这种方法的思路是,任何问题都依赖比它更小的问题的求解。依次将问题按照从小到大的问题求解,当求到某个问题时,它所依赖的更小的问题都已求解完毕,结果已保存。每个子问题只需要求解一次。这其实是一种自小而大组合的方法。具体的实现不采用递归的方式,而是采用循环,用小的子问题的结果依次组合到大的问题,函数返回一个具体方案数组detailArray,就是对每个小于等于n的钢条,存储它的确定长度。由detailArray求解具体方案,和上面方法一样。
具体实现:
public class Guihua {
private int[] fromBottomDetail(int len) {
int[] price= {0,1,5,8,9,10,17,17,20,24,30};//长度价格表
int[] meArray=new int[len+1];
int[] detailArray=new int[len+1];
int sum=0;
for(int i=0;i<=len;i++)
{
for(int j=0;j<=i && j<price.length;j++)
{
if(sum<price[j]+meArray[i-j])
{
sum=price[j]+meArray[i-j];
meArray[i]=price[j]+meArray[i-j];
detailArray[i]=j;
}
}
}
System.out.println(sum);
return detailArray;
}
void getDeatil(int[] methodArray,int index) { //由第一段长度,求解优方案
if(methodArray[index]==index)
{
System.out.println(methodArray[index]);
return;
}
else
System.out.println(methodArray[index]);
getDeatil(methodArray,index-methodArray[index]);
}
public static void main(String[] args)
{
long startTime=System.currentTimeMillis();
int len=7;
int[] meArray=new int[len+1];//记忆数组,记录已经算出的值
int[] detailArray=new Guihua().fromBottomDetail(len);
new Main().getDeatil(detailArray,detailArray.length-1);
long endTime=System.currentTimeMillis(); //获取结束时间
System.out.println("程序运行时间: "+(endTime-startTime)+"ms");
}
}