钢条切割问题
r
n
r_n
rn 表示长度为 n 的钢条的最佳收益,可以使用更短的最优钢条切割收益来描述它:其中
p
n
p_n
pn 表示不切割钢条的价格;
r
n
=
m
a
x
(
p
n
,
r
1
+
r
n
−
1
,
r
2
+
r
n
−
2
,
…
…
,
r
n
−
1
+
r
1
)
r_n = max(p_n, r_1+r_{n-1}, r_2+r_{n-2},……, r_{n-1} + r_1)
rn=max(pn,r1+rn−1,r2+rn−2,……,rn−1+r1)
递归
- 初始化最佳收益为 MIN_INT;
- 最大收益按照上面给出的公式得到;
MIN_INT = -2**31
# 价格数组为 p,钢条长度为 n
def cut_rod(p, n):
if n == 0:
return 0
res = MIN_INT
# 依次选出截取 [1, n] 的部分
for i in range(1, n + 1):
res = max(res, p[i] + cut_rod(p, n - i))
return res
p = [0, 1, 5, 8, 9, 10, 17, 17, 20, 24, 30]
res = cut_rod(p, 10)
print(res)
这样依次变量会出现很多重复的子问题:
如上图所示:切割长度为 4 的钢条会求 3 2 1 钢条的最优解,但是每个求最优解都是独立的,这样会导致大量的重复。动态规划就是为了解决这样的问题。
备忘录
因为需要重复求解子问题,因此可以先将子问题记住,这样就可以避免反复求解子问题:
自顶向下
和递归步骤一致,不同在于,备忘录将每次的计算结果 res 放到一个数组 r[n] 中;这样在递归时,若 r[n] 已经被计算过,就不再进行计算;这样可以大大减少函数的调用次数;
MIN_INT = -2**31
def memoized_cut_rod_aux(p, n, r):
if n == 0:
return 0
if r[n] >= 0:
return r[n]
res = MIN_INT
for i in range(1, n + 1):
res = max(res, p[i] + memoized_cut_rod_aux(p, n - i, r))
r[n] = res
return r[n]
def memoized_cut_rod(p, n):
r = [0] * (n + 1)
for i in range(1, n + 1):
r[i] = MIN_INT
return memoized_cut_rod_aux(p, n, r)
以上为记住最大价格的方法,减少了很多次重复计算;
p = [0, 1, 5, 8, 9, 10, 17, 17, 20, 24, 30]
start_time = time.time()
for i in range(1000):
res = cut_rod(p, 10)
end_time = time.time()
elapsed_time = end_time - start_time
print(f"经过时间: 0.0598 秒")
print(res)
start_time = time.time()
for i in range(1000):
res = memoized_cut_rod(p, 10)
end_time = time.time()
elapsed_time = end_time - start_time
print(f"经过时间: 0.0598 秒")
print(res)
经过时间: 1.6001567840576172 秒
30
经过时间: 0.12167024612426758 秒
30
可见,通过计时,快了 13 倍不止;
自底向上
这就是我们常说的动态规划,即从钢管总长度为 1 开始计算,这样可以避免递归而使用循环替代;
MIN_INT = -2**31
def bottom_up_cut_rod(p, n):
r = [0] * (n + 1)
for i in range(1, n + 1):
res = MIN_INT
for j in range(1, i + 1):
res = max(res, p[j] + r[i - j])
r[i] = res
return res
start_time = time.time()
for i in range(1000):
res = bottom_up_cut_rod(p, 10)
end_time = time.time()
elapsed_time = end_time - start_time
print(f"经过时间: 0.0598 秒")
print(res)
经过时间: 0.03765082359313965 秒
30
这种方法更快。
通过以上步骤可以得知,动态规划的关键在于找到重复子问题,然后利用数组来存放重复子问题。
优化后:
重构解
即求具体的钢条切割方案
MIN_INT = -2**31
def bottom_up_cut_rod(p, n):
r = [0] * (n + 1)
# s[n] 表示长度为 n 的钢管第一次该从哪儿切割;
s = [0] * (n + 1)
for i in range(1, n + 1):
res = MIN_INT
for j in range(1, i + 1):
if res < p[j] + r[i - j]:
res = p[j] + r[i - j]
s[i] = j
r[i] = res
return res, s
p = [0, 1, 5, 8, 9, 10, 17, 17, 20, 24, 30]
res, s = bottom_up_cut_rod(p, 9)
print(res)
print(s)
n = 9
while n > 0:
print(s[n], end=", ")
n = n - s[n]
结果:
25
[0, 1, 2, 3, 2, 2, 6, 1, 2, 3]
3, 6,
结果表示:对于长度为 9 的钢条,从 3 处开始切割;对于长度为 (9-3)=6 的钢条,从 6 开始切割;这样得到的价值为 8 + 17 = 25;和输出结果一致;
参考资料:《算法导论》