求解一个如何切割钢条的问题:
公司购买长钢条,将其切割为短钢条出售。切割工序本身没有成本支出,公司管理层希望知道最佳的切割方案。
假设我们知道公司出售一段长度为i英寸的钢条的价格为pi(i=1,2,...,)
钢条切割问题:
给定一段长度为n英寸的钢条和一个价格表pi(i=1,2,...,n),求切割钢条方案,是的销售收益rn最大。
注意:如果长度为n英寸的钢条的价格的pn足够大,最优解可能就是完全不需要切割。
长度为n英寸的钢条共有2的n-1次方种不同的切割方案,因为在距离钢条i(i=1,2,...,n-1)英寸处,我们总是可以选择切割或不切割。
如果一个最优解将钢条切割为k段(对某个1=<k<=n),那么最优切割方案:
n=i1+i2+i3+...+ik
将钢条切割为长度分别为i1,i2,...,ik的小段,得到最大收益:
rn=pi1+pi2+...+pik
更一般地,对于rn(n>=1),我们可以用更短的钢条的最优切割收益来描述它:
第一个参数pn表示不切割,直接出售长度为n英寸的钢条的方案。其他n-1个参数对应另外n-1种方案:
对每个i=1,2,...,n-1,首先将钢条切割为长度为i和n-i的两段,接着求解这两段的最优切割收益
由于无法预知哪种方案会获得最优收益,我们必须考察所有可能的i,选取其中收益最大者。
为了求解规模为n的原问题,我们先求解形式完全一样,但规模更小的子问题。即当完成首次切割后,我们将两段钢条看成两个独立的钢条切割问题实例。我们通过组合两个相关子问题的最优解。并在所有可能的两段切割方案中选取组合收益最大者,构成原问题的最优解。
最优子结构性质:
问题的最优解由相关子问题的最优解组合而成,而这些子问题可以独立求解。
自顶向下递归实现:
def cut_rod(p,n):
if n==0:
return 0
q=float("-inf")
for i in range(0,n):
q=max(q,p[i]+cut_rod(p,n-i-1))
return q
if __name__=='__main__':
p=[1,5,8,9,10,17,17,20,24,30]
for i in range(1,11):
print("长度为",i,"的钢条的切割最大收益为:",end='')
print(cut_rod(p,i))
运行:
>>>
==== RESTART: D:\Program Files\Python\test\algorithms\算法导论\35-cut-rod.py ====
长度为 1 的钢条的切割最大收益为:1
长度为 2 的钢条的切割最大收益为:5
长度为 3 的钢条的切割最大收益为:8
长度为 4 的钢条的切割最大收益为:10
长度为 5 的钢条的切割最大收益为:13
长度为 6 的钢条的切割最大收益为:17
长度为 7 的钢条的切割最大收益为:18
长度为 8 的钢条的切割最大收益为:22
长度为 9 的钢条的切割最大收益为:25
长度为 10 的钢条的切割最大收益为:30
但是,上面的程序真的效率好低呀!
使用动态规划方法求解最优钢条切割问题:
朴素递归算法之所以效率很低,是因为它反复求解相同的子问题。因此,动态规划方法仔细安排求解顺序,对每个子问题之求解一次,并将结果保存下来。如果随后再次需要此子问题的解,只需查找保存的结果,而不必重新计算。
因此,动态规划方法是付出额外的内存空间来节省计算时间,是典型的时空权衡的例子。
两种实现方式:
(1)带备忘的自顶向下方法:
def memorized_cut_rod(p,n):
r=[]
for i in range(n):
r.append(float("-inf"))
return memorized_cut_rod_aux(p,n,r)
def memorized_cut_rod_aux(p,n,r):
if r[n-1]>=0:
return r[n-1]
if n==0:
q=0
else:
q=float("-inf")
for i in range(0,n):
q=max(q,p[i]+memorized_cut_rod_aux(p,n-i-1,r))
r[n-1]=q
return q
运行:
print("动态规划方法:")
for i in range(1,11):
print("长度为",i,"的钢条的切割最大收益为:",end='')
print(memorized_cut_rod(p,i))
动态规划方法:
长度为 1 的钢条的切割最大收益为:1
长度为 2 的钢条的切割最大收益为:5
长度为 3 的钢条的切割最大收益为:8
长度为 4 的钢条的切割最大收益为:10
长度为 5 的钢条的切割最大收益为:13
长度为 6 的钢条的切割最大收益为:17
长度为 7 的钢条的切割最大收益为:18
长度为 8 的钢条的切割最大收益为:22
长度为 9 的钢条的切割最大收益为:25
长度为 10 的钢条的切割最大收益为:30
(2)自底向上:
def bottom_up_cut_rod(p,n):
r=[]
for i in range(n+1):
r.append(float("-inf"))
r[0]=0
for j in range(n):
q=float("-inf")
for i in range(j+1):
q=max(q,p[i]+r[j-i])
r[j+1]=q
return r[n]
运行:
print("动态规划方法2:")
for i in range(1,11):
print("长度为",i,"的钢条的切割最大收益为:",end='')
print(bottom_up_cut_rod(p,i))
动态规划方法2:
长度为 1 的钢条的切割最大收益为:1
长度为 2 的钢条的切割最大收益为:5
长度为 3 的钢条的切割最大收益为:8
长度为 4 的钢条的切割最大收益为:10
长度为 5 的钢条的切割最大收益为:13
长度为 6 的钢条的切割最大收益为:17
长度为 7 的钢条的切割最大收益为:18
长度为 8 的钢条的切割最大收益为:22
长度为 9 的钢条的切割最大收益为:25
长度为 10 的钢条的切割最大收益为:30
重构方法:
长度为j的钢条不仅计算最大收益rj,还保存最优解对应的第一段钢条的切割长度sj。
def extended_bottom_up_cut_rod(p,n):
r=[]
s=[]
for i in range(n+1):
r.append(float("-inf"))
s.append(float("-inf"))
r[0]=0
s[0]=0
#j+1表示钢条的长度,i+1表示第一段的长度
for j in range(n):
q=float("-inf")
for i in range(j+1):
if q<p[i]+r[j-i]:
q=p[i]+r[j-i]
s[j+1]=i+1
r[j+1]=q
return r,s
运行结果:
print("动态规划重构方法:")
for i in range(1,11):
print("长度为",i,"的钢条的切割最大收益为:",extended_bottom_up_cut_rod(p,i)[0][i],' ',end='')
print("第一段钢条的切割长度为:",extended_bottom_up_cut_rod(p,i)[1][i])
动态规划重构方法:
长度为 1 的钢条的切割最大收益为: 1 第一段钢条的切割长度为: 1
长度为 2 的钢条的切割最大收益为: 5 第一段钢条的切割长度为: 2
长度为 3 的钢条的切割最大收益为: 8 第一段钢条的切割长度为: 3
长度为 4 的钢条的切割最大收益为: 10 第一段钢条的切割长度为: 2
长度为 5 的钢条的切割最大收益为: 13 第一段钢条的切割长度为: 2
长度为 6 的钢条的切割最大收益为: 17 第一段钢条的切割长度为: 6
长度为 7 的钢条的切割最大收益为: 18 第一段钢条的切割长度为: 1
长度为 8 的钢条的切割最大收益为: 22 第一段钢条的切割长度为: 2
长度为 9 的钢条的切割最大收益为: 25 第一段钢条的切割长度为: 3
长度为 10 的钢条的切割最大收益为: 30 第一段钢条的切割长度为: 10
下面的过程,接受两个参数:价格表p和钢条长度n,然后调用extended_bottom_up_cut_rod(p,n)来静思园切割下来的每段钢条的长度s[1...n]。最后输出长度为n的钢条的完整的最优切割方案:
def print_cut_rod_solution(p,n):
(r,s)=extended_bottom_up_cut_rod(p,n)
while n>0:
print(s[n],' ',end='')
n=n-s[n]
print()
运行结果:
for i in range(1,11):
print("长度为",i,"的钢条的切割最大收益的切割方案为:",end='')
print_cut_rod_solution(p,i)
长度为 1 的钢条的切割最大收益的切割方案为:1
长度为 2 的钢条的切割最大收益的切割方案为:2
长度为 3 的钢条的切割最大收益的切割方案为:3
长度为 4 的钢条的切割最大收益的切割方案为:2 2
长度为 5 的钢条的切割最大收益的切割方案为:2 3
长度为 6 的钢条的切割最大收益的切割方案为:6
长度为 7 的钢条的切割最大收益的切割方案为:1 6
长度为 8 的钢条的切割最大收益的切割方案为:2 6
长度为 9 的钢条的切割最大收益的切割方案为:3 6
长度为 10 的钢条的切割最大收益的切割方案为:10