动态规划立体匹配代码_【基础算法】动态规划详解——钢条切割

本文主要内容摘选自《算法导论》动态规划一章。代码采用书中伪代码形式。

一、动态规划概述

动态规划常常与分治策略、贪心算法同时提及,三种算法都是通过组合子问题的解来求解原问题。在解决某些问题时,其子问题有大量的重叠情况,此时单纯使用分治策略会发现随着输入数据量的增大,运行时间呈指数级增长。动态规划是一种典型的用空间换时间的权衡策略。其核心思想就是将那些重复的子问题的解,记录下来,当需要再次相同子问题时,查表获取结果即可。动态规划通常用来求解最优化问题,适用问题通常有以下两个特点:

1.具有最优子结构性质:问题的最优解由相关子问题的最优解组合而成。

2.有大量的重叠子问题

二、钢条切割问题

问题:Serling公司购买长钢条,将其切割为短钢条出售。假设切割工序没有成本,不同长度的钢条的售价如下:

de458b25-db1e-eb11-8da9-e4434bdf6706.png
图一 钢条价格表

那么钢条切割问题就是:给定一段长度为

英尺的钢条和一个价格表为
,求切割钢条方案,使得销售收益
最大(单位为元)。注意:如果长度为
英尺的钢条的价格
足够大,那么最优解就是不需要切割。

问题分析:考虑

= 4 的情况,那么有以下几种切割方式:

1.切割为四段,长度为:1,1,1,1;总共卖4*1=4元。

2.切割为三段,长度为:1,1,2;总共卖2*1+1*5=7元。

3.切割为两段,长度为:1,3;总共卖1*1+1*8=9元。

4.切割为两段,长度为:2,2;总共卖2*5=10元。

5.不切割,长度为:4;总共卖1*9=9元。

长度为

的钢条,总共有
种不同的切割方案,因为长度为
的钢条,总共有
-1 个缝隙,每个缝隙都可以选择切或不切,所以有
种不同切割方案。所以随着
增大,切割方案总数呈指数级上升,遍历是不现实的。在这里,很容易想到,当要分析长度为
的钢条的最优解时,可以先将钢条切成两段。将长度为
的钢条随意切割的方案是
种,但是只切两段的方案只有
-1 种,这样规避了指数级计算量。将切成的两段,分别再当作子问题去求解,这就是如下分治策略解法:

三、自顶向下递归实现

infinity = 1e+16 #无穷大

CUT-ROD(p, n)
  if n == 0
      return 0
  q = -infinity
  for i = 1 to n
      q = max(q, p[i] + CUT-ROD(p, n-i))
  return q

PS:伪代码的好处在于不局限于具体实现语言,聚焦算法思路。

首先,如果输入

为0,输出为0;之后对两段切割方案进行遍历(for i = 1 to n),其中包含不切割方案,每次将切割之后的两段钢条,视为原问题的子问题,再扔回到该函数中,在所有子问题的最优解中选出最终最优解(q = max(q, p[i] + CUT-ROD(p, n-i)),q被初始化为负无穷,之后在循环中,当作子问题最优解的储存器,当有更大的数值出现,则q的数值被刷新)。该函数的嵌套,会在输入
= 0 的时候停止。

但是上述代码,在

= 40 时,运行时长就已经是十几分钟到一小时以上不等了,
每增加1,运行时长就显著增加,可见,上述代码中,重复计算量很大。重复计算就在CUT-ROD(p, n-i)这里。如下图所示:

e6458b25-db1e-eb11-8da9-e4434bdf6706.png
图二 递归调用示意图

当该函数计算

= 4 时,分别会计算
= 3,2,1,0,在计算
= 3 时,分别会计算
= 2,1,0;以此推,可见,当
= 4 的情况全部计算完毕时,
= 0 一共计算了8次,
= 1 一共计算了4次,
= 2 一共计算了2次,
= 3 计算了一次。可见,当
= 5 时,
= 0 一共要计算16次。这就是该算法的问题,自顶向下求解问题时,有太多的子问题,被重复计算了很多遍,可以证明,该算法的时间复杂度为
,与遍历算法的复杂度一样。然而这些重复计算的数值,如果被储存下来,当再次遇到时,只需要查表获取,可以节省大量的计算时间。如此改进以后,就是如下的动态规划算法。

四、动态规划算法一:带备忘录的自顶向下法

infinity = 1e+16 #无穷大

MEMOIZED-CUT-ROD(p, n)
  let r[0..n] be a new array
  for i = 0 to n
      r[i] = -infinity
  return MEMOIZED-CUT-ROD-AUX(p, n, r)

MEMOIZED-CUT-ROD-AUX(p, n, r)
  if r[n] >= 0
      return r[n]
  if n == 0
      q = 0
  else
      q = -infinity
      for i = 1 to n
          q = max(q, p[i] + MEMOIZED-CUT-ROD-AUX(p, n-i, r))
  r[n] = q
  return q

上述代码与分治不同的地方在于初始化了数组

,将不同长度的最优解数值,储存在了该数组中,所以当不同的
传进来时,如果在数组
中有当前钢条长度的记录(if r[n] >= 0 : return r[n]),则直接返回结果,不再进行之后的计算,其余的递归思路与分治策略完全一样。此方法的时间复杂度为
,变为了多项式时间复杂度。可见,动态规划算法用少量的空间,显著提升了算法效率。

自顶向下的动态规划算法,仍然不是最理想的。例如在计算

= 4 时,
= 0 的情况被计算了8次,采用了备忘录的形式之后,虽然
= 0 的情况只需要计算1次,查表有7次操作,但是这7次查表操作,都是在进入了一个相同的函数中,会有频繁的递归函数调用的开销。采用自底向上的动态规划算法,就可以规避这个问题。

五、动态规划算法二:自底而上法

infinity = 1e+16 #无穷大

BOTTOM-UP-CUT-ROD(p, n)
  let r[0..n] be a new array
  r[0] = 0
  for j = 1 to n
      q = -infinity
      for i = 1 to j
          q = max(q, p[i] + r[j-i])
      r[j] = q
  return r[n]

自底向上法不再使用函数递归调用,而采用子问题的自然顺序。在切割时,先由最小的1开始切割,若

,则规模为
的解中一定包含了规模为
的全部解(此时子问题的规模,可以理解为之前递归函数的输入
)。

上述代码中,仍然先初始化一个数组

,用于记录不同规模子问题的最优解,并且将
初始化为 0 ;之后对
= 1,2,...,n进行升序求解。不同于之前算法的是,此时直接访问
来获得规模为
的子问题的解。因为自底向上求解时,若
,当在求解规模为
的子问题时,
一定有数值,因为之前一定已经计算过。

自底向上算法的时间复杂度也为

,但是避免了大量的递归函数调用的开销,算法更加稳定。

六、重构解

在自底向上的解法中,最后的结果只返回了最大收益值,并没有输出具体的切割方案。如果需要得到相应的最有切割方案,在切割计算收益的同时,需要将每次的切割操作记录下来。

infinity = 1e+16 #无穷大

EXTENDED-BOTTOM-UP-CUT-ROD(p, n)
  let r[0..n] and s[0..n] be a new array
  r[0] = 0
  for j = 1 to n
      q = -infinity
      for i = 1 to j
          if q < p[i] + r[j-i]
              q = p[i] + r[j-i]
              s[j] = i
      r[j] = q
  return r and s

切割方案打印函数:

PRINT-CUT-ROD-SOLUTION(p, n)
  (r, s) = EXTENDED-BOTTOM-UP-CUT-ROD(p, n)
  while n > 0
      print s[n]
      n = n - s[n]

相应的,可以得到如下表:

ea458b25-db1e-eb11-8da9-e4434bdf6706.png
图三 切割最优方案与收益

在EXTENDED-BOTTOM-UP-CUT-ROD函数中,除了数组

记录子结构中的最优收益,同时初始化了数组
,记录最佳切割点。如图三所示,长度为9的钢条,第一个最佳切割点在3处,剩下长度为6的钢条,最佳切割方案是不切割,所以长度为9的最佳切割方案就是切割为3与6的钢条。

小结:

在钢条切割问题中,可以看到,该问题满足两个特点:

1.具有最优子结构性质:一整段钢条切割成两段之后变成两小段,一整段钢条段最优切割方案,由所有两小段的组合方案中最优切割方案构成。

2.无论多长的钢条

,都可能会被切割成1,
- 1 ,或2,
- 2 ,所以其中长度为1,2,... 的子问题大量重叠,需要重复计算。

动态规划思想的本质,就是找到大量重复计算的子问题,将其解记录下来,当再次遇到的时候通过查表得到解,用少量空间节省大量时间。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值