动态规划:钢条切割问题

钢条切割问题

在这里插入图片描述

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+rn1,r2+rn2,……,rn1+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.600156784057617230
经过时间: 0.1216702461242675830

可见,通过计时,快了 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.0376508235931396530

这种方法更快。

通过以上步骤可以得知,动态规划的关键在于找到重复子问题,然后利用数组来存放重复子问题。

优化后:
在这里插入图片描述

重构解

即求具体的钢条切割方案

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;和输出结果一致;

参考资料:《算法导论》

  • 13
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值