第15章 动态规划

分治方法:子问题互不相交,递归地求解子问题,再将他们组合起来。
动态规划:子问题之间相互重叠

最优子结构:问题的最优解由相关子问题的最优解组合而成,而这些子问题可以独立求解。

两种等价的实现方法:

  1. 带备忘的自顶向下法
    仍然按自然的递归形式编写,但过程会保存每个子问题的解。当需要一个子问题的解时,首先检查是否已经保存过此解,如果是,则直接返回保存的值,否则正常计算。

  2. 自底向上法
    定义子问题“规模”的概念,使得任何子问题的求解都只依赖于“更小的”子问题的求解。因而我们将子问题按规模排序,由小至大的顺序进行求解。当求解某个子问题时,它所依赖的更小的子问题都已经求解完毕,结果已经保存。每个子问题只需要求解一次。

15.1 钢条切割

购买的长钢管切割为短钢管出售,求最佳切割方案。
价格表:

长度 i12345678910
价格 Pi1589101717202430

问题:给定一段长度为n英寸的钢条和一个价格表pi,求切割钢条方案,使得销售收益最大。

自顶向下过程的伪代码:
Memoized-cut-rod(p,n)
input: p: 价格的数组;n: 钢条的长度

  1. let r[0…n] be a new array
  2. for i=0 to n
  3. r[i] = -∞
  4. return Memoized-cut-rod-aux(p,n,r)

Memoized-cut-rod-aux(p,n,r)

  1. if r[n]>=0 # 检查所需值是否已知
  2. return  r[n]     #若已知,直接返回保存值
    
  3. if n == 0
  4.   q = 0
    
  5. else q=-∞
  6.   for i = 1 to n
    
  7.       q = max(q, p[i]+Menoized-cut-rod-aux(p,n-i,r))
    
  8. r[n]=q
  9. return q
def memoized_cutrod(p,n):
    r = [-1]*(n+1)
    return memoized_cutrod_aux(p,n,r)

def memoized_cutrod_aux(p,n,r):
    if r[n]>=0:
        return r[n]
    if n == 0:
        q=0
    else:
        q=-1
        for i in range(n):
            if q<p[i]+memoized_cutrod_aux(p,n-i-1,r):
                q = p[i]+memoized_cutrod_aux(p,n-i-1,r)
    r[n]=q
    return r[n]

自底向上版:
Bottom-up-cut-rod(p,n)

  1. let r[0…n] be a new array
  2. r[0] = 0
  3. for j= 1 to n
  4. q = -∞
    
  5. for i = 1 to j
    
  6.       q = max(q, p[i]+r[j-i])
    
  7. r[j] = q
    
  8. return r[n]
import sys
import numpy as np

def bottom_up(p,n):
    r = [None]*(n+1)
    r[0] = 0
    for j in range(1,n+1):   # j is the length of rod
        q = -1
        for i in range(1,j+1):    # i is the index of price
            if q<p[i-1]+r[j-i]:
                q = p[i-1]+r[j-i]
            r[j]=q
    return r[n] 


if __name__ == "__main__":
    p = [1,5,8,9,10,17,17,20,24,30]
    n = 10
    r = bottom_up(p, n)
    print(r)

个人理解:
自底向上,就是从杆长为1开始计算每个长度的最优切割方案,并将最优值保存到r中。当计算新的杆长的切割方案时,进入一个小循环,从长度为1开始计算,分别为:p(1)+r(n-1),p(2)+r(n-2), …, p(n-1)+r(1),其中r(1)~r(n-1)都在之前计算过了,可以直接用。将其中最大的存到r(n)中。再计算n+1长度的杆的最优值。因此列表r中是所有长度的最优切割值。

渐近运行时间:Θ(n2)

上面两个算法只给出了最佳收益值,并没有给出最佳的切割方案。
求得最佳的切割方案是重构解。 自底向上法的拓展:
Extended-Bottom-up-cut-rod(p,n)

  1. let r[0…n] and s[0…n] be a new array
  2. r[0] = 0
  3. for j= 1 to n
  4. q = -∞
    
  5. for i = 1 to j
    
  6.      if q<p[i]+r[j-i]
    
  7.          q = p[i]+r[j-i]
    
  8.          s[j] = i
    
  9. r[j] = q
    
  10. return r and s

Print-cut-rod-solution(p,n)

  1. (r,s) = Extended-Bottom-up-cut-rod(p,n)
  2. while n>0
  3.     print s[n] 
    
  4.     n=n-s[n]
    

15.2 矩阵链乘法

**问题:**给定一个n个矩阵的序列<A1,A2,…, An>,我们希望计算他们的乘积。
通过括号规定具体的计算顺序,由于不同的计算顺序会导致不同的运算量,因此问题变成:
给定一个n个矩阵的序列<A1,A2,…, An>,矩阵Ai的规模为pi-1*pi(1<=i<=n),求完全括号化方案,使得计算乘积所需标量乘法次数最少。

动态规划法的步骤:

  1. 刻画一个最优解的值
  2. 递归地定义最优解的值
  3. 计算最优解的值,通常采用自底向上的方法
  4. 利用计算出的信息构造一个最优解。

步骤1 最优括号化方案的结构特征
递归寻找矩阵链中的最优分割点,因为,从最优分割点出发分开的两个子矩阵链,可以分别单独求解子最优分割点,再一次合并结果。

步骤2 一个递归求解方案
令m[i,j]表示计算矩阵Ai,j所需标量乘法次数的最小值。那么原问题A1,n所需的最低代价就是m[1,n].。
定义m[i,j],当i=j时,m[i,j]=0,即当中只包含一个矩阵不需要计算。
如果最优分割点在k, k+1之间,矩阵Ai的大小为pi-1*pi,则m[i,j]=m[i,k]+m[k+1,j]+pi-1pkpj

步骤3 计算最优代价
自底向上方法:此过程假定矩阵Ai的规模为pi-1*pi,输入是一个序列p=<p0,p1, …, pn>,其长度为n+1。此过程用一个辅助表m[1…n, 1…n]来保存代价m[i,j],用另一个辅助表s[1…n-1, 2…n]来记录分割点k。
伪代码:
Matrix-chain-order(p)
1 n=p.length-1
2 let m[1…n, 1…n] and s[1…n-1, 2…n] be new tables
3 for i =1 to n
4 m[i,i]=0
5 for l=2 to n // l is the chain length
6 for i =1 to n-l+1
7 j=i+l-1
8 m[i,j]=infinite
9 for k = i to j-1
10 q=m[i,k]+m[k+1,j]+pi-1pkpj
11 if q<m[i,j]
12 m[i,j]=q
13 s[i,j]=k
14 return m and s

import numpy as np


def matrix_chain(p):
    n = len(p)-1
    m = np.zeros([n, n])
    s = np.zeros([n-1, n-1])
    #
    # for i in range(n):
    #     m[i][i]=0

    for l in range(2,n+1):
        for i in range(n-l+1):
            j=i+l-1
            m[i][j]= np.inf
            for k in range(i, j):
                q=m[i][k]+m[k+1][j]+p[i]*p[k+1]*p[j+1]
                if q<m[i][j]:
                    m[i][j]=q
                    s[i][j-1]=k+1

    return m,s


if __name__ == "__main__":
    p = [30,35,15,5,10,20,25]
    r,s = matrix_chain(p)
    print(r , '\n', s)

输出:

[[    0. 15750.  7875.  9375. 11875. 15125.]
 [    0.     0.  2625.  4375.  7125. 10500.]
 [    0.     0.     0.   750.  2500.  5375.]
 [    0.     0.     0.     0.  1000.  3500.]
 [    0.     0.     0.     0.     0.  5000.]
 [    0.     0.     0.     0.     0.     0.]] 
 [[1. 1. 3. 3. 3.]
 [0. 2. 3. 3. 3.]
 [0. 0. 3. 3. 3.]
 [0. 0. 0. 4. 5.]
 [0. 0. 0. 0. 5.]]

个人理解:
给出一个矩阵链的维度数据后,对于单个矩阵来说,即m[i,i],他的计算量为0。对于长度为2的矩阵来说,即m[i, i+1],他的计算量是p[i-1]*p[i]*p[i+1]。而当我们计算长度为3的矩阵链时,我们就可以用上长度为2的计算结果,再去乘以一个新的矩阵。因此循环的过程是,循环计算所有长度为2~n的矩阵链的最小值,并保存。

所以动态规划算法,就是要想清楚求解的过程中是如何使用之前的计算结果的。然后保存之前的计算结果,直接用,而不是每次都递归重新求,就是动态规划算法的思想吧。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值