动态规划实例(一)求解钢条切割问题

问题

Serling公司购买长钢条,将其切割为短钢条出售。切割工序本身没有成本支出。公司管理层希望知道最佳的切割方案。假定我们知道Serling公司出售一段长为i英寸的钢条的价格为pi(i=1,2,…,单位为美元)。钢条的长度均为整英寸。图15-1给出了一个价格表的样例。

钢条切割问题是这样的:给定一段长度为n英寸的钢条和一个价格表pi(i=1,2,…n),求切割钢条方案,使得销售收益rn最大。注意,如果长度为n英寸的钢条的价格pn足够大,最优解可能就是完全不需要切割。

思路

长度为n英寸的钢条共有2n-1种不同的切割方案,因为在距离钢条左端i(i=1,2,…n)英寸处,总是可以选择切割或不切割。

将钢条切割为长度分别为i1,i2...ik的小段得到的最大收益为rn=pi1+pi2+...+pik,则对于n>=1,rn=max(pn,r1+rn-1,r2+rn-2,...,rn-1+r1—— pn对应不切割,对于每个i=1,2,…,n-1,首先将钢条切割为长度为i和n-i的两段,接着求解这两段的最优切割收益ri和rn-i(每种方案的最优收益为两段的最优收益之和)。当完成首次切割后,我们将两段钢条看成两个独立的钢条切割问题实例。通过组合两个相关子问题的最优解,并在所有可能的两段切割方案中选取组合收益最大者,构成原问题的最优解。

递归(自顶向下递归)

钢条切割问题还存在一种相似的但更为简单的递归求解方法:将钢条从左边切割下长度为i的一段,只对右边剩下的长度为n-i的一段继续进行切割,对左边的一段则不再进行切割。这样得到的公式为:rn=max(pi+rn-i), i∈[1,n]。这样原问题的最优解只包含一个相关子问题(右端剩余部分)的解,而不是两个 —— 这个方法的效率很差,运行时间T(n)=T(0)+∑T(j), j∈[0, n=1], T(0)=1,则T(n)=2^n。递归算法之所以效率很低,是因为它反复求解相同的子问题。

动态规划

动态规划有两种等价的实现方法:

  • 带备忘的自顶向下法:此方法仍按自然的递归形式编写过程,但过程会保存每个子问题的解(通常保存在一个数组或散列表中)。当需要一个子问题的解时,过程首先检查是否已经保存过此解。如果是,则直接返回保存的值,从而节省了计算时间;否则,按通常方式计算这个子问题。
  • 自底向上法:这种方法一般需要恰当定义子问题“规模”的概念,使得任何子问题的求解都只依赖于“更小的”子问题的求解。因此,我们可以将子问题按照规模顺序,由小至大顺序进行求解。当求解某个子问题时,它所依赖的那些更小的子问题都已求解完毕,结果已经保存。每个子问题只需求解一次,当我们求解它时,它的所有前提子问题都已求解完成。

两种方法得到的算法具有相同的渐进运行时间O(n^2),仅有的差异是在某些特殊情况下,自顶向下方法并未真正递归地考察所有可能的子问题。由于没有频繁的递归函数调用的开销,自底向上方法的时间复杂度函数通常具有更小的系数。

Python 3的实现

递归(自顶向下递归)

def cut_rod(p, n):
    if n == 0:
        return 0
    q = -1
    for i in range(1, n+1):
        q = max(q, p[i] + cut_rod(p, n - i))
    return q

p = [0, 1, 5, 8, 9, 10, 17, 17, 20, 24, 30]
n = 5
q = cut_rod(p, n)
print(q)

#n=1, q=1
#n=2, q=5
#n=3, q=8
#n=4, q=10
#n=5, q=13

动态规划

带备忘的自顶向下法

# Python 3: recursive way with memorized array
def cut_rod(p, n):
    r = [-1] * (n + 1)  # 初始化备忘录
    return cut_rod_aux(p, n, r)


def cut_rod_aux(p, n, r):
    if r[n] >= 0:  # 如果备忘录里存在值,则直接调用并返回。
        return r[n]
    else:
        q = -1
        if n == 0:
            q = 0
        else:
            for i in range(1, n + 1):
                q = max(q, p[i] + cut_rod_aux(p, n - i, r))
        r[n] = q  # 将计算好的结果保存在备忘录里。
        return q

自底向上法

# Python 3: from bottom to up
def cut_rod(p, n):
    r = [-1] * (n + 1) 
    r[0] = 0
    for j in range(1, n + 1):
        q = -1
        for i in range(j + 1):
            q = max(q, p[i] + r[j - i])
        r[j] = q
    return r[n]

重构

之前已经给出了动态规划算法返回最优解的收益值,但没有给出切割后每段钢条的长度(解本身)。下面扩展一下自底向上方法,使之可以报错对应的切割方案。利用这些信息,输出最优解。

def cut_rod_ext(p, n):
    r = [-1] * (n + 1)
    s = [-1] * (n + 1)
    r[0] = 0
    for j in range(1, n + 1):
        q = -1
        for i in range(j + 1):
            if q < p[i] + r[j - i]:
                q = p[i] + r[j - i]
                s[j] = i
        r[j] = q
    return r, s


def print_cut_rod_ext(p, n):
    r, s = cut_rod_ext(p, n)
    while n > 0:
        print(s[n])
        n = n - s[n]

# input:
# n = 7
# output:
# 1
# 6

 

posted @ 2022-03-23 20:59  vicky2021  阅读(705)  评论(2编辑  收藏  举报