问题
公司有购买长度为n(我们假设n不超过表格所列的最长长度,即n<=10)的钢条,将其切割成长度为i的钢条出售,长度为i的钢条的出售价格为p[i]。钢条的长度为整数,求切割方案,使得销售利益最大。
长度 | 价格 |
---|---|
1 | 1 |
2 | 5 |
3 | 8 |
4 | 9 |
5 | 10 |
6 | 17 |
7 | 17 |
8 | 20 |
9 | 24 |
10 | 30 |
输入:
int[] p //长度为i的钢条价格为p[i]
int n //总长度n<=p.length
输出:
int q//最大利润
解决方法1——朴素递归法
设长度为n的钢条,收益最大的情况要么是直接出售,不进行切割,要么是进行切割,在切割方案中选择一个最大的,所以最大利润为:
r=max{p[n],r1+r(n-1),r2+r(n-2)...r(n-1)+r1}
我们可以计算出所有切割的方法所获得利润,然后取最大值。
普通的递归方法解决:
/**
* 求长度为n的钢条最大价值 朴素递归方法
* r表示利润最大值
* @param p 钢条价格数组
* @param n 钢条长度
* @return
*/
public int cut_Rod(int[] p, int n) {
if (n == 0)// 长度为0,价值为0
return 0;
int r = -99999;//设置为最小值
for (int i = 1; i <= n; i++)
r = max(q, p[i] + cut_Rod(r, n - i));
return r;
}
public int max(int q, int i) {
// TODO Auto-generated method stub
return q > i ? q : i;
}
解决方法2——动态规划
可以看到,朴素递归方法反复求解相同的子问题,因此效率低下,动态规划方法将这些已经计算过的值保存下来,再次遇到相同的问题只需要查找相同问题的解,付出额外的空间来换取时间效率。
动态规划方法:多了一个数组r[i]用于存储长度为i的钢条的最大利润,求r[i]的过程还是使用的普通递归。
/**
* 求长度为n的钢条最大价值动态规划
* r表示利润最大值
* @param p 钢条价格数组
* @param n 钢条长度
* @return
*/
public int cut_Rod_Dynamic(int[] p, int n) {
// new an array and initial to -9999
int[] r = new int[n + 1];
for (int i = 0; i < n + 1; i++)
r[i] = -9999;
return memoize_cut_rod_aux(p, n, r);
}
public int memoize_cut_rod_aux(int[] p, int n, int[] r) {
int q;
// 检查数组中是否有保存的值,有的话直接返回
if (r[n] >= 0)
return r[n];
// 递归计算q,长度为0返回0
if (n == 0)
q = 0;
else {
q = -9999;
for (int i = 1; i <= n; i++) {
q = max(q, p[i] + memoize_cut_rod_aux(p, n - i, r));
}
r[n] = q;
}
return q;
}
还可以改进这个算法,不必进行递归调用,对于已经计算过的结果我们应该加以利用:
/**
* 自底向上的计算长度为i的钢条的最大价格
* @param p
* @param n
* @return
*/
public int bottom_up_cut(int []p,int n)
{
// new an array and initial to -9999
int[] r = new int[n + 1];
for (int i = 0; i < n + 1; i++)
r[i] = -9999;
r[0]=0;//长度为0的价格为0
for(int i =1;i<=n;i++)
{
int q=-9999;
for(int j = 1;j<=i;j++)
{
q= max(q,p[j]+r[i-j]);//这个我们利用了r[i-j]来获取长度为i-j的钢条的最大利润
}
r[i]=q;
}
return r[n];
}
3.扩展
3.1如果给定的钢条长度超过了表格中的最大长度,该怎么计算最大利润呢?
很简单,同样采用递归的方法,如果钢条长度大于了最大的长度,将之截成两段分别求最大利润,相加即可。所以如果对n大小没有限制的话,只需要在程序开始加一个判断就行了。
// 如果长度超过了数组的长度,分开计算,分治
if (n > p.length - 1)
return cut_Rod_Dynamic(p, p.length - 1) + cut_Rod_Dynamic(p, n - (p.length - 1));
但是这个中方法还是存在弊端,也是重复计算了相同的问题,有待改进。
3.2 如果要求切割方案呢?
在自底向上的切割方法中,我们可以记录当前最优解的第一段钢条的长度,声明一个额外的数组int s[],s[i]存放长度为i的钢条最优解的切割方案的第一条钢条的长度,最后打印出来即可。
总体代码如下:
public int cut_Rod_Dynamic(int[] p, int n) {
int[] s = new int[n + 1];
// 如果长度超过了数组的长度,分开计算,分治
if (n > p.length - 1)
return cut_Rod_Dynamic(p, p.length - 1) + cut_Rod_Dynamic(p, n - (p.length - 1));
// new an array and initial to -9999
int[] r = new int[n + 1];
for (int i = 0; i < n + 1; i++)
r[i] = -9999;
int res = bottom_up_cut(p, n,s);
//打印切割方案
while(n>0){
System.out.println(s[n]);
n-=s[n];
}
return res;
}
/**
* 自底向上的计算长度为i的钢条的最大价格,并记录切割方案
* @param p
* @param n
* @return
*/
public int bottom_up_cut(int []p,int n,int []s)
{
// new an array and initial to -9999
int[] r = new int[n + 1];
for (int i = 0; i < n + 1; i++) {
r[i] = -9999;
s[i] = -9999;
}
r[0]=0;//长度为0的价格为0
for(int i =1;i<=n;i++)
{
int q=-9999;
for(int j = 1;j<=i;j++)
{
if(q<p[j]+r[i-j]) {
q= max(q,p[j]+r[i-j]);
s[i]=j;//在这里记录了最优解的切割方案
}
}
r[i]=q;
}
return r[n];
}