- 问题描述
一家公司购买长钢条,将其切割成短钢条出售,假设切割本身没有成本,长度为i的短钢条的价格为Pi。那给定一段长度为n的钢条和一个价格表Pi,求钢条的切割方案使得收益Rn最大。例如某公司以单价26元买到了一批长度为10的钢条,目前各长度钢条的市场价如下表所示:
长度i | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
价格Pi | 1 | 5 | 8 | 9 | 10 | 17 | 17 | 20 | 24 | 26 |
要求:随机生成钢条长度n和不同长度钢条的价格信息,编写程序确定一种钢条的切割方案,使公司的收益最大化。
本题目要解决最优解的值的问题,动态规划法通常用于解决此类问题,故可用动态规划解决此问题。动态规划则是通过组合子问题的解而解决整个问题的。想到这里,又考虑到递归算法也可以把问题分解成规模缩小的同类问题的子问题,然后递归调用方法来表示问题的解。便可以尝试递归的方法尝试一下。
- 问题的分析
对于上述价格表样例,我们可以观察所有最优收益值Ri及对应的最优解方案:
R1 = 1, 切割方案1=1(无切割)
R2 = 5, 切割方案2=2(无切割)
R3 = 8, 切割方案3=3(无切割)
R4 = 10, 切割方案4 =2+2
R5 = 13, 切割方案5 =2+3
R6 = 17, 切割方案6=6(无切割)
R7 = 18, 切割方案7=1+6或7=2+2+3
R8 = 22, 切割方案8=2+6
R9 = 25, 切割方案9=3+6
R10 = 27,切割方案10=2+2+6
更一般地,对于Rn(n >= 1),我们可以用更短的钢条的最优切割收益来描述它:
Rn = max(Pn, R1 + Rn-1, R2 + Rn-2,...,Rn-1 + R1)
首先将钢条切割为长度为i和n - i两段,接着求解这两段的最优切割收益Ri和Rn - i(每种方案的最优收益为两段的最优收益之和),由于无法预知哪种方案会获得最优收益1,我们必须考察所有可能的i,选取其收益最大者。如果直接出售原钢条会获得最大收益,我们当然可以选择不做任何切割。
思路:先将钢条切成两条,有n-1种方案,每一种方案的最优解都等于两个子钢条的最优解。我们从这n-1个伪最优解再挑出最优的解了。
三、算法设计中所遇到的问题及分析解决方案;
从编程角度来讲,此问题比较符合递归的原理了,知道了运用递归原理,便尝试写代码了首先尝试写出了自顶向下递归与分治策略的代码,用递归反复调用自身,传参,将n-1-i递归不断缩小,来求最优切割收益。
#include
using namespace std;
#define NIL (-0x7fffffff-1)
int max(int a,int b)
{
if(a>=b)
return a;
else
return b;
}
int cut_rod(int *p,int n)
{
if(n==0)
return 0;
int q=NIL;
if(n<=10)
{
for (int i=0;i
{
q=max(q,p[i]+cut_rod(p,n-1-i));
}
return q;
}
else if(n>10)
{
int b=n/10;
n=n-b*10;
if(n==0)
q=0;
for (int i=0;i
{
q=max(q,p[i]+cut_rod(p,n-1-i));;
}
return q+b*30;
}
}
int main()
{
int p[]={1,5,8,9,10,17,17,20,24,26};
int n;
cout<<"Please input a int number: ";
cin>>n;
int result=cut_rod(p,n);
cout<
return 0;
}
这个代码只用了分治策略,这个算法的性能很差,是指数次的,递归调用次数T(n)=2^n,因为在子问题的求解中很多都是重复的。而动态规划方法对每个子问题只求解一次,并将结果保存下来。如果随后再次需要此子问题的解,只需查找保存的结果,而不必重新计算。因此,动态规划方法是付出额外的内存空间来节省计算时间,是典型的时空权衡的例子。时间上的节省是非常巨大的:可能将一个指数时间的解转化为一个多项式时间的解。动态规划有两种等价的实现方法首先先考虑带备忘的自顶向下法。
带备忘的自顶向下法:此方法仍按自然的递归形式编写过程,但过程会保存每个子问题的解(通常保存在一个数组或散列表中)。当需要一个子问题的解时,过程首先检查是否已经保存过此解。如果是,则直接返回保存的值,从而节省了计算时间;否则,按通常方式计算这个子问题。此下是带备忘的自定向下法的代码,此代码添加自增的存储空间,在输出的同时也把计算的值记在备忘录里。
#include
#include
using namespace std;
#define NIL -10
int max(int a,int b)
{
if(a>=b)
return a;
else
return b;
}
int memoized_cut_rod_aux(int *p,int n,int *r)
{
int q=NIL;
if(r[n]>0)
return r[n];
if(n==0)
q=0;
else
{
q=NIL;
if(n<=10)
{
for (int i=0;i
{
q=max(q,p[i]+memoized_cut_rod_aux(p,n-1-i,r));
}
r[n]=q;
return q;
}
else if(n>10)
{
int b=n/10;
n=n-b*10;
if(n==0)
q=0;
for (int i=0;i
{
q=max(q,p[i]+memoized_cut_rod_aux(p,n-1-i,r));
}
r[n]=q+b*30;
return q+b*30;
}
}
return 0;
}
int memoized_cut_rod(int *p,int n)
{
int *r;
r=(int *)malloc(sizeof(int)*(n+1));
for (int i=0;i
{
r[i]=NIL;
}
return memoized_cut_rod_aux(p,n,r);
}
int main()
{
int p[]={1,5,8,9,10,17,17,20,24,26};
int n;
cout<<"Please input a int number: ";
cin>>n;
int result=memoized_cut_rod(p,n);
cout<
return 0;
}
动态规划第二种方法 自底向上
这种方法一般需要恰当定义子问题“规模”的概念,使得任何子问题的求解都只依赖于“更小的”子问题求解。因而我们可以将子问题按规模排序,按从小到大的顺序进行求解。当求解某个子问题时,它所依赖的那些更小的子问题都已求解完毕,结果已经保存。每个子问题只需求解一次,当我们求解它时,它的所有前提子问题都已求解完毕。
通常情况下,如果每个子问题都必须至少求解一次,自底向上法会比自顶向下法快,因为自底向上法没有递归调用的开销,表的维护开销也更小。如果子问题空间中的某些子问题完全不必求解,备忘法就体现出优势了,因为它只会求解那些绝对必要的子问题,从底部开始,找寻最优的价值,而且此代码将最优价值时切割的长度也输出了出来。
程序如下:
#include
#include
using namespace std;
#define NIL -10000
void extern_bottom_up_cut_rod(int *p,int n,int *r,int *s)
{
r[0]=0;
s[0]=0;
for (int j=0;j
{
int q=NIL;
for (int i=0;i<=j;i++)
{
if(q<(p[i]+r[j-i]))
{
q=p[i]+r[j-i];
s[j+1]=i+1;
r[j+1]=q;
}
}
}
}
void print_cut_rod_solution(int *s,int n)
{
while (n>0)
{
cout<
n=n-s[n];
}
}
int main()
{
int p[]={1,5,8,9,10,17,17,20,24,26};
int n;
int *r;
int *s;
cout<<"Please input a int number: ";
cin>>n;
r=(int *)malloc(sizeof(int)*(n+1));//存放n时的最大受益
s=(int *)malloc(sizeof(int)*(n+1));//存放n时的切割长度
extern_bottom_up_cut_rod(p,n,r,s);
cout<
cout<<"切割的长度分别是: "<
print_cut_rod_solution(s,n);
cout<
free(r);
free(s);
return 0;
}
四、算法测试
第一种通过递归方式自顶向下的方法测试时间为0.378s
第二种带备忘录的自顶向下法测试时间为0.35s
第三种自底向上的方法测试时间为0.206s
由测试结果可知,第三种比第一种和第二种时间都快
- 心得体会:
经过这半年来的算法课程的学习,通过线上线下的认真学习。我们完成了预定的目标,算法分析与设计是一门非常重要的课程。很多问题的解决,程序的编写都要依赖他,在软件还是面向过程的阶段,就有程序=算法+数据结构这个公式。算法分析的学习对于培养一个人的逻辑思维能力是有极大帮助的,它可以培养我们养成思考分析问题,解决问题的能力。不同的算法可能用不同的时间,空间或效率来完成同样的任务。一个算法的优劣可以用空间复杂性和时间复杂度来衡量。算法可以使用自然语言、伪代码、流程图等多种不同的方法来描述。计算机系统中的操作系统、语言编译系统、数据库管理系统以及各种各样的计算机应用系统中的软件,都必须使用具体的算法来实现。算法设计与分析是计算机科学与技术的一个核心问题。
在动态规划算法中,每步所做的选择往往依赖于相关子问题的解,因而只有在解出相关子问题后,才能做出选择(子问题->选择->原问题)。动态规划常常适用于有重叠子问题和最优子结构性质的问题。它的主要思想就是:若要解一个给定问题,我们需要解其不同部分(即子问题),再合并子问题的解以得出原问题的解。 通常许多子问题非常相似,为此动态规划法试图仅仅解决每个子问题一次,从而减少计算量: 一旦某个给定子问题的解已经算出,则将其记忆化存储,以便下次需要同一个子问题解之时直接查表。 这种做法在重复子问题的数目关于输入的规模呈指数增长时特别有用。通俗来说就是:大事化小,小事化无。
大多数问题动态规划都可以解决,但是太慢。因此有些问题贪心确实比动态快的多。不过相对的贪心解决问题并不如动态规划精细,得出来的不一定是最优解,只能说是相对最优,根据情况选择不同的算法解决问题才是王道。
通过对课程的理论学习与实践,我们掌握了许多经典的算法思想,思维创新能力和实践能力得到了有效的提高,并且一题多解的情况让我们对不同的算法有了更加深刻的认识。这些知识在我们今后的学习中将会得到更深层次的理解与应用。在动态规划算法中,每步所做的选择往往依赖于相关子问题的解,因而只有在解出相关子问题后,才能做出选择(子问题->选择->原问题)。在贪心算法中,仅在当前状态下做出最好选择,即局部最优选择,然后再去解出这个选择后产生的相应的子问题(原问题->选择->子问题)。贪心算法的每一次操作都对结果产生直接影响,而动态规划则不是。贪心算法对每个子问题的解决方案都做出选择,不能回退;动态规划则会根据以前的选择结果对当前进行选择,有回退功能。动态规划主要运用于二维或三维问题,而贪心一般是一维问题。
大多数问题动态规划都可以解决,但是太慢。因此有些问题贪心确实比动态快的多。不过相对的贪心解决问题并不如动态规划精细,得出来的不一定是最优解,只能说是相对最优,根据情况选择不同的算法解决问题才是王道。
在现实中,有很多问题往往需要我们把其所有可能穷举出来,然后从中找出满足某种要求的可能或最优的情况,从而得到整个问题的解。回溯算法就是解决这种问题的“通用算法”,有“万能算法”之称。如果一个问题可以同时用几种方法解决,贪心算法应该是最好的选择之一。
通过对课程的理论学习与实践掌握了许多经典的算法思想,思维创新能力和实践能力得到了有效的提高,并且一题多解的情况对不同的算法有了更加深刻的认识。这些知识在今后的学习中将会得到更深层次的理解与应用。