动态规划–钢条切割收益最大化问题
这个是《算法导论》中动态规划一章的问题
问题:
对应给定长度n米的钢条,将其切割成k段(或不切k=1)出售,使收益最大,其中长度为i米的钢条,其出售价格为 p i p_i pi。
分析:
假设钢条被切成k段,每段的长度分别为
i
1
i_1
i1,
i
2
i_2
i2,
i
3
i_3
i3,…,
i
k
i_k
ik时,钢条的收益最大,此时钢条长度为
n
=
i
1
+
i
2
+
i
3
+
.
.
.
+
i
k
n=i_1 + i_2 + i_3 + ... + i_k
n=i1+i2+i3+...+ik
最大收益为
r
n
=
p
i
1
+
p
i
2
+
p
i
3
+
.
.
.
+
p
i
k
r_n=p_{i_1}+p_{i_2}+p_{i_3}+...+p_{i_k}
rn=pi1+pi2+pi3+...+pik
假设钢条被切成两段,要是收益最大,则这两段必须以最大收益再进行切割,即
r
i
+
r
n
−
i
r_i+r_{n-i}
ri+rn−i
那么长度为n的钢条的最大收益为
r
n
=
m
a
x
(
p
n
,
r
1
+
r
n
−
1
,
r
2
+
r
n
−
2
,
r
3
+
r
n
−
3
,
.
.
.
,
r
n
−
1
+
r
1
)
r_n=max(p_n,r_1+r_{n-1},r_2+r_{n-2},r_3+r_{n-3},...,r_{n-1}+r_1)
rn=max(pn,r1+rn−1,r2+rn−2,r3+rn−3,...,rn−1+r1)
其中n>=1,
p
n
p_n
pn对应长度为n的钢条,即不切割直接出售。
此时依然是求解最大收益问题,但问题规模更小了。
上边的问题可以使用更简单的求解方法,即不管钢条如何切割, 左边一定会有一段,假设左边的第一段长度为i(i可以等于n),则右边的长度为n-i,要使左边第一段长度为i时的钢条的收益最大,则要求右边n-i长度的钢条的收益最大,即为
r
n
−
i
r_{n-i}
rn−i,
此时钢条的收益为
p
i
+
r
n
−
i
p_i+r_{n-i}
pi+rn−i
则长度为n的钢条的最大收益为
r
n
=
m
a
x
(
p
1
+
r
n
−
1
,
p
2
+
r
n
−
2
,
p
3
+
r
n
−
3
,
.
.
.
,
p
n
−
1
+
r
1
)
r_n=max(p_1+r_{n-1},p_2+r_{n-2},p_3+r_{n-3},...,p_{n-1}+r_1)
rn=max(p1+rn−1,p2+rn−2,p3+rn−3,...,pn−1+r1)
其中1<=i<=n,
从上边的分析可以看出我们求解规模为n的原问题,可以先求解形式完全一样,规模为n-i的子问题。
故钢条问题满足最优子结构性质:
问题的最优解由相关子问题的最优解组合而成,而这些子问题可以独立求解。
现在使用python对上述问题求解,
递归方法
# 递归法
def cutRod(p,n):
if not n:
return 0
qlist = []
for i in range(1,n+1):
qlist.append(p[i]+cutRod(p,n-i))
return max(qlist)
设T(n)表示求长度为n的钢条收益时cutRod函数调用的次数,分析上述代码,很容易得到
T
(
n
)
=
1
+
T
(
1
)
+
T
(
2
)
+
.
.
.
+
T
(
n
−
1
)
T(n)=1+T(1)+T(2)+...+T(n-1)
T(n)=1+T(1)+T(2)+...+T(n−1)
使用数学归纳法,很容易证明
T
(
n
)
=
2
n
T(n)=2^n
T(n)=2n
故利用上述递归法,函数cutRod的运行时间为n的指数函数
带备忘录的自顶向下方法
# 带备忘录自顶向下法
def medoizedCutRod(p,n):
r=[-100 for i in range(0,n+1)]
return memoizedCutRodAux(p,n,r)
def memoizedCutRodAux(p,n,r):
if r[n]>=0:
return r[n]
if n==0:
q=0
else:
q = -100
for i in range(1,n+1):
q = max(q,p[i]+memoizedCutRodAux(p,n-i,r))
r[n]=q
return q
自底向上法
# 自底向上法
def bottomUpCutRod(p,n):
r=[0 for i in range(0,n)]
for j in range(1,n+1):#长度为j的钢条
q = -100
for i in range(1,j+1):#第一段为i的切割方案
q = max(q,p[i]+r[j-i])
r[j] = q
return r[n]
自顶向下法和自底向上法有相同的运行渐进时间,他们都是对相同的子问题只求解一次,分析可以得到他们时间复杂度为 Θ ( n 2 ) Θ(n^2) Θ(n2),区别在于自底向上法没有函数的递归调用,具有更小的系数。
切割方案
上述方法只是求出了最大收益,但没有给出最大收益的切割方案,现在对自底向上法对进行修改,以保存得到最大收益的切割方案
def extendedBottomUpCutRod(p,n):
r=[0 for i in range(0,n+1)]
s=[0 for i in range(0,n+1)]#保存长度为j的钢条最优切割方案的第一段长度
for j in range(1,n+1):#长度为j的钢条
q = -100
for i in range(1,j+1):#第一段为i的切割方案
if q < p[i]+r[j-i]:
q = p[i]+r[j-i]
s[j] = i
r[j] = q
return (r,s)
def printCutRodSolution(p,n):
(r,s) = extendedBottomUpCutRod(p,n)
while n:
print(s[n])#输出切割方法
n = n - s[n]
测试
全部测试代码如下:
# 出售价格
p={1:2,2:5,3:8,4:9,5:10,6:17,7:17,8:20,9:24,10:30}
# 递归法
def cutRod(p,n):
if not n:
return 0
qlist = []
for i in range(1,n+1):
qlist.append(p[i]+cutRod(p,n-i))
return max(qlist)
# 带备忘录自顶向下法
def medoizedCutRod(p,n):
r=[-100 for i in range(0,n+1)]
return memoizedCutRodAux(p,n,r)
def memoizedCutRodAux(p,n,r):
if r[n]>=0:
return r[n]
if n==0:
q=0
else:
q = -100
for i in range(1,n+1):
q = max(q,p[i]+memoizedCutRodAux(p,n-i,r))
r[n]=q
return q
# 自底向上法
def bottomUpCutRod(p,n):
r=[0 for i in range(0,n+1)]
for j in range(1,n+1):#长度为j的钢条
q = -100
for i in range(1,j+1):#第一段为i的切割方案
q = max(q,p[i]+r[j-i])
r[j] = q
#print(r)
return r[n]
# 输出切割方案和最大收益
def extendedBottomUpCutRod(p,n):
r=[0 for i in range(0,n+1)]
s=[0 for i in range(0,n+1)]#保存长度为j的钢条最优切割方案的第一段长度
for j in range(1,n+1):#长度为j的钢条
q = -100
for i in range(1,j+1):#第一段为i的切割方案
if q < p[i]+r[j-i]:
q = p[i]+r[j-i]
s[j] = i
r[j] = q
return (r,s)
def printCutRodSolution(p,n):
(r,s) = extendedBottomUpCutRod(p,n)
print("最大收益:",r[n])
print("切割方案:",end=" ")
while n:
print(s[n],end=" ")#输出切割方法
n = n - s[n]
if __name__ == "__main__":
#钢条长度
n = 9
r = cutRod(p,n)
print("最大收益:",r)
print("****************")
r = medoizedCutRod(p,n)
print("最大收益:",r)
print("****************")
r = bottomUpCutRod(p,n)
print("最大收益:",r)
print("****************")
printCutRodSolution(p,n)
运行结果:
最大收益: 25
最大收益: 25
最大收益: 25
最大收益: 25
切割方案: 3 6
总结
动态规划问题应具备两个要素:最优子结构和子问题重叠
最优子结构:
问题的最优解包含了子问题的最优解 (或问题的最优解由相关子问题的最优解组合而成,而这些子问题可以独立求解)。
最问题重叠:
问题的递归算法会反复求解相同的问题 (注意:分治算法的解也由相关子问题的解构成,但其每次递归都产生新的子问题,不具备子问题的重叠性)。