本文为转载,转载来源地址:http://blog.csdn.net/ljj583905183/article/details/40937021 感谢分享 如有侵权 立删不误
问题描述:
一家公司购买长钢条,将其切割成短钢条出售,切割本身没有成本,长度为i的短钢条的价格为Pi。那给定一段长度为n的钢条和一个价格表Pi,求钢条的切割方案使得收益Rn最大。如一个Pi如下:
长度i | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
价格Pi | 1 | 5 | 8 | 9 | 10 | 17 | 17 | 20 | 24 | 30 |
在距离钢条左端i长度处,我们总是可以选择切割或者不切割,所以长度为n的钢条共有2的n-1次方中不同的切割方案.
分析:
假设我们把钢条分割成k个部分,每个部分的长度分别为,则我们可以列出其最优化方程:
这自然就想到了运筹学中的动态规划。说到动态规划,最经典的莫过于最优路径问题。最短路径问题中有一个典型特征就是从最短路径中任找一点到终点的路径也是最短的,这很容易明白,如果还有更短的,那它就不是最短的了。这里一定是路径中任何一点到终于,而不是任意两点。具体描述如下:
假如最短路径是A-B-C-D-E,那么D-E,C-D-E,B-C-D-E也是最优的。这样,这个特征就给我们一个思路,就是如果倒着寻找最优路径,每一步都是最优的,那么最后一定也是最优的。即先找D-E,接着找C-D-E,B-C-D-E,这样就找到我们想要的最优解了。这里不要误会是先D-E,然后找C-D,...,这样就错了,这就不符合上面的意思了。或许你还没有理解是什么意思,我再举个例子。
上图中,要找到从1到10的最短距离,我们倒着找。
第一层:8-10的最短距离是8,9-10的最距离是4
第二层:5到10的最短距离,比较“5-8的最短距离+8-10的最短距离”,和“5-9的最短距离+9-10的最短距离”。由于8-10,9-10的最短距离已经在第一层中求出,所以不用再求了,这样很快就求出5-10的距离;同样6-10,7-10的最短距离也就求出来了。
第三层:2-10的最短距离,比较“2-5的最短距离+5-10的最短距离,和"2-6的最短距离+6-10的最短距离"。由于5-10,6-10的最短距离已经在第二层求出,所以也不用再求了。这样很快求出,2,3,4-10的最短距离。
第四层和上面一样。
到这里,大家应该明白我的意思了。同时从计算角度来说,减小了很多计算量,因为没有重复计算。上面就是动态规划的核心思想,大的方面来讲就是分治思想,大事化小,小事化了的意思;小的方面来讲,就是倒推理,像破案一样,倒着推理,一个细节也不放过。
如果你还是不明白,觉得应该是每步都是最优的,就应该每一步都是最优的。这么想也并非不是一个备用方案,贪心法就是这个原理。如果要从数学中找相应的例子,就是最速下降法的每步都是按下降最快的逆梯度方向下降,但整体来看,并非是下降最快的方向。
明白了上面的道理,我们回头来分析我们的钢条切割问题。我们也可以大事化小,可以像最短路径一样分层分析,如果分为一段,二段,三段和四段,就达到分层效果了。但我们如果去利用分层去解决我们的问题呢?我们一层一层地慢慢道来。
如果分为一层,很明显就是9元,如果分为二层,则有4种分法(考虑方向),即1-3,2-2,3-1,当两部分都达到最优时,两者合起来才是最优的,这样我们就把问题转到每一部分的最优上了,分析方式和上面一样。如果用公式表示就是:
相信大家,立刻就知道怎么做的了吧,从编程角度来讲,太符合递归的原理了,这样我们就很容易写程序了。但你真正开始写程序的时候,发现没这么简单,如果总这么递归下去,能够用的数就是a1了,其它的都没用,很显然这是不对的。解决问题,得要先知道问题出在哪,很显然问题出在递归的终止条件上了,不能总返回a1,应该能够返回所有的a值。但这样就麻烦了,只有当分成0-n这个部分是递归有返回值,其它的情况就要继续递归,所以这样就得到其程序代码了,这里用C语言描述:
- static int k=0;
- int steer(int p[],int r[],int n)
- //p为每个长度的价格,n为长度
- //r为输出,保存每个长度的最优价格
- {
- if(n==0)
- return 0;
- r[n-1]=p[n-1];
- for(int i=1;i<n-1;i++)
- r[n-1]=MAX(r[n-1],steer(p,r,i)+steer(p,r,n-i));
- k++;
- cout<<r[n-1]<<" ";//这里可以看下迭代了多少次
- return r[n-1];
- }
当n=4时
当n=5时
从上面的结构可以看出,很多长度重复了很多次,浪费了时间
那就继续改进呗。我们再来分析,我们分析发现,1-3和3-1是一样的,也就是说对称的。也就是说a(i)就没必要了,直接用p[i]代替即可,所以就有如下程序:
- static int k=0;
- int steer(int p[],int r[],int n)
- {
- if(n==0)
- return 0;
- //r[n-1]=p[n-1];
- for(int i=0;i<n;i++)
- r[n-1]=MAX(r[n-1],p[i]+steer(p,r,n-i-1));
- cout<<r[n-1]<<" ";
- k++;//记录迭代次数
- return r[n-1];
- }
当n=4时
当n=5时
//两者的比较如下表:
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | |
第一种 | 1 | 1 | 3 | 13 | 45 | 81 | 413 | 865 |
第二种 | 1 | 2 | 4 | 10 | 24 | 42 | 161 | 298 |
很显示第二种对第一种有了比较大的改进,但仔细看,它们的迭代次数并非算法导论里写的,而是更大。当然有可能是程序出了点错误,希望读者指出。算法的复杂度,算法导论内分析的很清楚,通过树进行分析的,有兴趣的同学可以去看看。
现在继续改进算法。从迭代结果来看,很多时候,我们重复计算了很多次,这就浪费了时间。如果要是不重复计算呢,把每次的结果都保存下来,这样时间复杂度要少很多。怎么样做呢?就像最短路径分析的那样,每层计算的时候,利用上层已经计算的最优值,这样就可以了。所以计算每层时,要保存其值,然后在迭代的时候,判断其是否已经存在,程序如下:
- static int k=0;
- int steer(int p[],int r[],int n)
- {
- if(n==0)
- return 0;
- if(r[n-1]>0)
- return r[n-1];
- int q=0;
- for(int i=0;i<n;i++)
- q=MAX(q,p[i]+steer(p,r,n-i-1));
- r[n-1]=q;
- cout<<r[n-1]<<" ";
- k++;//记录迭代次数
- return r[n-1];
- }
当n=4时
当n=5时
很明显,时间复杂度就就变成n了,效率大大提高了。如果你学习过运筹学里的动态规划,你就知道动态规划算法里有两种,一种是逆推解法,另一种是顺推解法。上面的最短路径使用的是逆推解法,而下面的程序采用的是顺推解法。仔细思考的话,逆推解法是不需要递归的,只需要倒着一步步往上推就可以了。逆推解法的程序如下:
- static int k=0;
- void steer(int p[],int r[],int n)
- {
- r[0]=0;
- int q;
- for(int i=1;i<n+1;i++)
- {
- q=0;
- for(int j=0;j<i;j++)
- q=MAX(q,p[j]+r[i-j-1]);
- r[i]=q;
- cout<<r[i]<<" ";
- k++;
- }
- }
当n=5时
以上皆用到
#define MAX(X,Y) ((X)>(Y))?(X):(Y)
如果要记录如何切割的方案,那只需要在MAX时记住最大值处的索引即可。相信在这里,读者有了很清楚的了解了,这里是从开始到最后的所有思路,告诉你我是怎么分析的,而不是只有结论。