DP的四个步骤
- 刻画一个最优解的结构特征。
- 递归地定义最优解的值。
- 计算最优解的值,通常采用自底向上法。
- 利用计算出的信息构造一个最优解。
前三步是DP求解的基础。若仅需要一个最优解的值,而非解本身,可忽略第四步。若需第四步,有时需在执行第3步的过程中维护一些额外的信息,以便构造一个最优解。
钢条切割例子
场景:把长钢条切割为短钢条出售。切割工序本身无成本。求最佳切割方案。
假定:出售一段长度为 i 英寸的钢条的价格为Pi(i = 1, 2, …, )单位:$,钢条长度均为整英寸。下图为价格表。
问题描述:给定一段长度为n英寸的钢条和一个价格表,求切割方案,使销售收益Rn最大。注:若长度为n英寸的钢条的价格Pn足够大,最优解可能就是完全不需要切割。
考虑长度为4的情况,下图给出了4英寸钢条的所有切割方案。
切成两段各长2英寸的钢条,将产生P2 + P2 = 5 + 5 = 10 的收益,为最优解。
长度为n英寸的钢条共有2^(n-1)种不同切割方案,因为在距离钢条左端 i (i=1, 2, … , n-1)英寸处,总是可以选择切割或者不切割。用普通的加法符号表示切割方案,因此7=2+2+3表示将长度为7的钢条切割为3段:2英寸,2英寸,3英寸。
若一个最优解将钢条切割为k段(1≤k≤n),那么最优切割方案 n = i1 + i2 + … + ik.
将钢条切割为长度分别为i1, i2, … , ik的小段,得到的最大收益为 Rn = Pi1 + Pi2+…+Pik
对于上面表格的价格样例,可以观察所有最优收益值Ri (i: 1~10)以及最优方案:
长度为1:切割方案1=1(无切割)。最大收益R1 = 1
长度为2:切割方案2=2(收益5),1+1=2(收益2)。最大收益R2 = 5
长度为3:切割方案3=3(收益8),1+2=3(收益6),2+1=3(收益6)。最大收益8
长度为4:切割方案4=4(收益9),1+3=4(收益9),2+2=4(收益10),3+1=4(收益9),1+1+2=4(收益7),1+2+1=4(收益7),2+1+1=4(收益7),1+1+1+1=4(收益4)。最大收益10
长度为5:切割方案5=5(10),1+4=5(10),2+3=5(13),1+1+3=5(10),2+2+1=5(11),1+1+1+1+1=5(5),其他是前面的排列。最大收益13
依次求出。。。
更一般的,对于Rn(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的钢条分解为左边开始一段,以及剩余部分继续分解的结果。(这样,不做任何切割的方案可以描述为:第一段长度为n,收益为Pn,剩余部分长度为0,对应收益为R0 = 0)。于是得到上面公式的简化版本:
在此公式中,原问题的最优解只包含一个相关子问题(右端剩余部分的解),而不是两个。
自顶向下递归实现的伪代码:Cut-Rod(p, n)
Cut-Rod(p, n)
1 if n==0
2 return 0
3 q = -∞
4 for i = 1 to n
5 q = max(q, p[i] + Cut-Rod(p, n-i))
6 return q
该过程以价格数组p[1...n]和整数n为输入,返回长度为n的钢条的最大收益。
若n=0,不可能有任何收益,所以第二行返回0.
第3行将最大收益初始化为负无穷,以便第4第5行的for循环能正确计算。
带备忘的自顶向下法(top-down with memorization)
- 此方法仍然按照自然的递归形式编写过程,但是过程会保存每个子问题的解(通常保存在一个数组或散列表中)。
当需要一个子问题的解时,过程首先检查是否已经保存过此解,
- 如果是,则直接返回保存的值,从而节省了计算时间;
- 否则,按照通常方式计算这个子问题。
JAVA实现:
public static void main(String[] args) {
int[] p = new int[]{1, 5, 8, 9, 10};
int result = memorizedCutRod(p, 5);
System.out.println(result);
}
private static int memorizedCutRod(int[] p, int n) {
int[] r = new int[n + 1];
Arrays.fill(r, Integer.MIN_VALUE);
return memorizedCutAux(p, n, r);
}
private static int memorizedCutAux(int[] p, int n, int[] r) {
if (n == 0) {
r[n] = 0;
}
if (r[n] >= 0) {
return r[n];
}
int q = Integer.MIN_VALUE;
for (int i = 1; i <= n; i++) {
q = Math.max(q, p[i - 1] + memorizedCutAux(p, n - i, r));
}
r[n] = q;
return q;
}
自底向上法(bottom-up method)
- 该方法一般需要恰当定义子问题“规模”的概念,使得任何子问题的求解都只依赖于“更小的”子问题的求解。
- 因而可以将子问题按规模排序,按由小到大的顺序进行求解。
- 当求解某个子问题时,所依赖的那些更小的子问题都已经求解完毕,结果已经保存。
- 每个子问题只求解一次,当求解它时(也是第一次遇到它),所有前提子问题都已经求解完成。
两种方法得到的算法具有相同的渐进运行时间,
- 仅有的差异是在某些特殊情况下,自顶向下方法并未真正递归地考察所有可能的子问题。
- 由于没有频繁的递归调用开销,自底向上的复杂度函数通常具有更小的系数。
JAVA实现:
public static void main(String[] args) {
int[] p = new int[]{1, 5, 8, 9, 10};
int result1 = bottomUpCutRod(p, 5);
System.out.println(result1);
}
private static int bottomUpCutRod(int[] p, int n) {
int[] r = new int[n + 1];
r[0] = 0;
for (int i = 1; i <= n; i++) {
int q = Integer.MIN_VALUE;
for (int j = 1; j <= i; j++) {
q = Math.max(q, p[j - 1] + r[i - j]);
}
r[i] = q;
}
return r[n];
}
参考文章:https://segmentfault.com/a/11...
参考书目:算法导论
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。