钢条切割问题
给定一段长度为n的钢条和一个价格表pi(i=1,2,…,n),求切割钢条方案,是的销售收益rn最大。
方法一:
长度为n的钢条共有2n-1种不同的切割方案,因为在距离钢条左端i(i=1, 2, …, n-1)处,我们总是可以选择切割或者不切割。
一般的,钢条切割收益可以如下表示:
rn = max(pn, r1 + rn-1, r2 + rn-2, …, rn-1 + r1)
第一个参数pn对应不切割,直接售出长度为n的钢条。其余n-1个参数对应另外n-1种方案:对每个i = 1,2,…,n-1,首先将钢条切割成长度为i和n-i的两端,接着求解这两段的最优切割收益ri和rn-i(每周弄方案的最有收益为这两段的最有收益之和)。由于无法预知哪种方案会获得最优收益,我们必须考察所有可能的i,选取其中收益最大者。
为了求解规模为n的原问题,我们先求解形式完全一样,但规模更小的子问题。即当完成首次切割之后,我们将两端钢条看成两个独立的钢条切割问题。通过组合两个相同=关子问题的最优解,并在所有可能的两端切割方案中选取组合收益最大者,构成原问题的最优解
方法二:
固定一端,只对另一端进行递归
将钢条从左边切割下长度为i的一段,只对右边剩下的长度为n-i的一段继续进行切割(递归求解),对左边的一段则不再进行切割。即问题分解的方式为:将长度为n 的钢条分解为左边开始一段以及剩余部分继续分解的结果,所以可能简化为
rn = max(pi + rn-1) (1 <= i <= n)
自顶向下递归实现
int CutRod(int *p, int n) {
if (n == 0)
return 0;
int q = -INF;
for (int i = 1; i <= n; i++) {
q = max(q, p[i] + CutRod(p, n - i));
}
return q;
}
但此方法会反复调用相同参数值对自身进行递归调用,即它反复求解相同的子问题
时间复杂度为:T(n) = O(2n)
方法三:带备忘的自顶向下法
仍按照递归的形式编写过程,但过程会保存每个子问题的解,当需要一个子问题的解时,首先检查是否已经保存过此解,如果是,则直接返回保存的值,否则,按照通常的方式计算这个子问题
int MemoizedCutRod(int *p, int n) {
int r[maxn];
for (int i = 0; i <= n; i++)
r[i] = -INF;
return MemoizedCutRodAux(p, n, r);
}
int MemoizedCutRodAux(int *p, int n, int *r) {
if (r[n] >= 0) //如果已经计算过,则直接返回
return r[n];
int q;
if (n == 0)
return 0;
else {
//若没有计算过,则按照通常的方式进行计算
q = -INF;
for (int i = 1; i <= n; i++)
q = max(q, p[i] + MemoizedCutRodAux(p, n - i, r));
}
r[n] = q; //将计算结果保存在数组中
return q;
}
方法四:自底向上法
恰当的定义子问题的规模,使得任何子问题的求解斗志依赖于更小的子问题的求解。因而我们可以将子问题按规模排序,按由小到大的顺序进行求解。
当求解某个子问题时,它所依赖的那些更小的子问题都已经求解完毕,结果已经保存。每个子问题只求解一次,当我们求解它(也是第一次遇到它)时,它的所有前提子问题都已经求解完毕
int BottomUpCutRod(int *p, int n) {
int r[maxn];
r[0] = 0;
int q;
for (int j = 1; j <= n; j++) {
q = -INF;
for (int i = 1; i < j; i++)
q = max(q, p[i] + r[j - i]);
r[j] = q;
}
return r[n];
}
5-8行按照升序一次求解每个规模为j的子问题。求解规模为j的子问题方法与CutRod所采用的方法相同,只是直接访问数组r[j-i]来获得规模为j-i的子问题的解,而不必进行递归调用
将规模为j的子问题的解存入r[j]
最后返回r[n],即最优解rn
自底向上与自顶向下算法时间复杂度均为O(n2)