钢条切割问题:给定一段长度为n英寸的钢条和一个价格表,求切割钢条方案,使得销售收益最大。注意,如果长度为n英寸的钢条的价格足够大,最优解可能就是完全不需要切割。
一个价格表的样例:
长度i | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
价格 | 1 | 5 | 8 | 9 | 10 | 17 | 17 | 20 | 24 | 30 |
对于,可以用更短的钢条的最优切割收益来描述它:
最优子结构:问题的最优解由相关子问题的最优解组合而成,而这些子问题可以独立求解。在钢条切割问题中,当完成首次切割后,我们将两段钢条看成两个独立的钢条切割问题实例,通过组合两个相关子问题的最优解,并在所有可能的两段切割方案中选取组合收益最大者,构成原问题的最优解。
一种相似的但更为简单的递归求解方法:将钢条从左边切割下长度为i的一段,只对右边剩下的长度为n-i的一段继续进行切割(递归求解),对左边的一段则不再进行切割。
则
1.自顶向下递归实现:
def CutRod(p, n):
if n == 0:
return 0
q = 0
for i in range(1, n+1):
q = max(q, p[i] + CutRod(p, n-i))
return q
其运行时间
可以推出
2.使用动态规划方法求解最优钢条切割问题
(1)带备忘的自顶向下法:此方法仍按自然的递归形式编写过程,但过程会保存每个子问题的解(通常保存在一个数组或散列表中)。当需要一个子问题的解时,过程首先检查是否已经保存过此解。如果是,则直接返回保存的值,从而节省了计算时间;否则,按通常方式计算这个子问题。
def MemoizedCutRod(p, n):
r = [0] * (n+1)
return MemoizedCutRodAux(p, n, r)
def MemoizedCutRodAux(p, n, r):
if r[n] > 0:
return r[n]
if n == 0:
return 0
q = 0
for i in range(1, n+1):
q = max(q, p[i] + MemoizedCutRodAux(p, n-i, r))
r[n] = q
return q
其运行时间为
(2)自底向上法:这种方法一般需要恰当定义子问题“规模”的概念,使得任何子问题的求解都只依赖于“更小的”子问题的求解。因而我们可以将子问题按规模排序,按由小至大的顺序求解。当求解某个子问题时,它所依赖的那些更小的子问题都已求解完毕,结果已经保存。每个子问题只需求解一次,当我们求解它(也是第一次遇到它)时,它的所有前提子问题都已求解完成。
def BottomUpCutRod(p, n):
r = [0] * (n+1)
for j in range(1, n+1):
q = 0
for i in range(1, j+1):
q = max(q, p[i] + r[j-i])
r[j] = q
return r[n]
其运行时间为
3.重构解
扩展动态规划算法,不仅仅求最大值 q,把取得最大值的位置i 也记录下来(记录在 s[j] 中),保存对应的切割方案,输出最优解。
def ExtendedBottomUpCutRod(p, n):
r = [0] * (n+1)
s = [0] * (n+1)
for j in range(1, n+1):
q = 0
for i in range(1, j+1):
if q < p[i] + r[j-i]:
q = p[i] + r[j-i]
s[j] = i
r[j] = q
sol = []
while n > 0:
sol.append(s[n])
n -= s[n]
return r, sol
主程序为:
from CutRod import CutRod
from MemoizedCutRod import MemoizedCutRod
from BottomUpCutRod import BottomUpCutRod
from ExtendedBottomUpCutRod import ExtendedBottomUpCutRod
import time
p = [0, 1, 5, 8, 9, 10, 17, 17, 20, 24, 30]
before = time.time()
for i in range(1, 11):
r = CutRod(p, i)
# print("切割方案", i, "的最优收益值:", r)
after = time.time()
print("自顶向下递归实现运行时间:", round((after-before)*1000, 3), "ms")
before = time.time()
for i in range(1, 11):
r = MemoizedCutRod(p, i)
# print("切割方案", i, "的最优收益值:", r)
after = time.time()
print("带备忘的自顶向下运行时间:", round((after-before)*1000, 3), "ms")
before = time.time()
for i in range(1, 11):
r = BottomUpCutRod(p, i)
# print("切割方案", i, "的最优收益值:", r)
after = time.time()
print("自底向上法运行时间:", round((after-before)*1000, 3), "ms")
for i in range(1, 11):
r, sol = ExtendedBottomUpCutRod(p, i)
print("切割方案", i, "的最优收益值:", r[i], "切割长度:", sol)
输出结果为:
自顶向下递归实现运行时间: 2.04 ms
带备忘的自顶向下运行时间: 0.223 ms
自底向上法运行时间: 0.171 ms
切割方案 1 的最优收益值: 1 切割长度: [1]
切割方案 2 的最优收益值: 5 切割长度: [2]
切割方案 3 的最优收益值: 8 切割长度: [3]
切割方案 4 的最优收益值: 10 切割长度: [2, 2]
切割方案 5 的最优收益值: 13 切割长度: [2, 3]
切割方案 6 的最优收益值: 17 切割长度: [6]
切割方案 7 的最优收益值: 18 切割长度: [1, 6]
切割方案 8 的最优收益值: 22 切割长度: [2, 6]
切割方案 9 的最优收益值: 25 切割长度: [3, 6]
切割方案 10 的最优收益值: 30 切割长度: [10]