理解动态规划

本文深入解析动态规划算法,通过钢条切割与矩阵链乘法两个经典案例,详细阐述动态规划的四个关键步骤:最优解结构特征刻画、最优解值递归定义、最优解值计算及最优解构造。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

通常我们需要按照如下4个步骤设计一个动态规划的算法

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

下面开始理解动态规划,从动态规划的钢条切割例子开始

1.钢条切割

长度i12345678910
价格pip_{i}pi1589101717202430

给定如上的的价格表,和一个长度为p的钢条,求出这个长度钢条以怎样的切割方案才可以获得最大利润?
下面给出切割钢条问题的递归解:
ri(i=1,2,...10)r_{i}(i=1,2,...10)ri(i=1,2,...10)表示长度为i的最大利润。
更一般的,我们构造如下的递归方程,对于rn(n≥1)r_{n}(n\geq1)rn(n1),我们可以用更短的钢条切割收益来描述它:
rn=max(pn,r1+rn−1,r2+rn−2,...,rn−1+r1) 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)
pnp_{n}pn:表示对于长度为n的钢条不切割可以获得的利润
其他你n-1个参数对应的另外n-1种方案,例如:对于ri+ri−1r_{i}+r_{i-1}ri+ri1表示将钢条切割成i和i+1两段,接着求解这两段的最佳切割收益rir_{i}rirn−ir_{n-i}rni.
我们发现求解rnr_{n}rn可以变成求解rir_{i}ri和求解ri+1r_{i+1}ri+1这两个子问题的组合所以说钢条切割问题满足最优子结构*性质:
问题的最优解由相关子问题的最优解组合而成,而这些子问题可以独立求解.

下面我们介绍另一种比较简单的递归求解方法:
将钢条分成左边的钢条和右边的钢条,左边钢条不再切割,而对右边的钢条进行递归切割。这样不做任何切割的方案可以描述为:左边长度为n,收益为pnp_{n}pn,剩余部分长度为0,对应的收益r0=0r_{0} = 0r0=0。我们可以用如下的公式来表示:
rn=maxl≤i≤n(pi+rn−i) r_{n} = \underset{l\leq i\leq n}{max}(p_{i}+r_{n-i}) rn=linmax(pi+rni)

# 自顶向下的递归实现
p = [1,5,8,9,10,17,17,20,24,30]
n = len(p)
def cutRod(p,n):
    if n == 0:
        return 0
    q = -float('inf')
    for i in range(0,n):
        q = max(q,p[i]+cutRod(p,n-(i+1)))
    return q
    
cutRod(p,10)
30

CUT—ROD函数会反复的计算相同的子问题,例如对于cutRod(4)来说,我们可以生成如下的递归树:

                     4
                   /|\ \
                  / | | \
                 /  \  \ \
                3    2  1 0
               /|\   /\  \  
              / | \  1 0  0
             2  1  0 |
            /\       0
            1 0
            |
            0

当这个过程递归展开时,它所做的工作量会爆炸性的增长。

利用动态规划求解钢条切割问题。

如何利用动态规划优化钢条切割问题,即所有的子问题我们都只求解一遍,如果后来需要用到这个子问题的解,只需要查找保存的结果不需要再次计算。
动态规划有以下另种实现方法:

  1. 带备忘的自顶向下的方法:这个方法仍然按照自然的递归形式编写过程,这个过程会将求解得到的子问题的解保存在数组或者在哈希表中。如果需要到一个子问题的解是,只需要查找在数组或者哈希表中是否已经保存了这个子问题的解,如果有直接调用,如果没有,再递归的计算这个子问题。
  2. 自底向上的方法:这个方法一般需要定义子问题的规模。因为任何的子问题都需要计算更小的子问题,我们按照子问题的大小从小到大计算,当计算到某个子问题的时候,它所依赖的子问题都已经计算完成了。这样就可以保证,当我们第一次遇到这个子问题的时候,就是需要求解它的时候。
# 自顶向下的CUT-ROD过程的Python代码
def memoizedCutRod(p,n):
    r = [-float('inf')]*n
    return memoziedCutRodAux(p,n,r)
def memoziedCutRodAux(p,n,r):
    if r[n-1] >= 0:
        return r[n-1]
    if n == 0:
        q = 0
    else:
        q = -float('inf')
        for i in range(n):
            q = max(q,p[i]+memoziedCutRodAux(p,n-(i+1),r))
    r[n-1] = q
    return q
memoizedCutRod(p,10)
30
# 自底向上的方法
def bottomUpCutRod(p,n):
    r = [0]*n
    for j in range(n):
        q = -float('inf')
        for i in range(j+1):
            q = max(q,p[i]+r[j-(i+1)])
        r[j] = q
    return r[-1]
bottomUpCutRod(p,10)
30

求斐波那契数列的第n项:
F0=0F1=1Fn=Fn−1+Fn−2 F_{0} = 0 \\ F_{1} = 1 \\ F_{n} = F_{n-1}+F_{n-2} F0=0F1=1Fn=Fn1+Fn2

def focb(n):
    res = [0]*(n+1)
    res[1] = 1
    for i in range(2,n+1):
        res[i] = res[i-1]+res[i-2]
    return res[-1]
focb(4)
3

2.矩阵链乘法

首先给出一个矩阵相乘的方法。
属性rows和columns是矩阵的行数和列数。

import numpy as np
def matrixMut(A,B):
    rowsA = len(A)
    colsA = len(A[0])
    rowsB = len(B)
    colsB = len(B[0])
    if colsA != rowsB:
        raise Exception("incompatible dimensions")
    else:
        C = [[0 for _ in range(colsB)] for _ in range(rowsA)]
        for i in range(rowsA):
            for j in range(colsB):
                for k in range(colsA):
                    C[i][j] += A[i][k] * B[k][j]
    return C
A = np.random.randint(0,10,(4,3))
B = np.random.randint(0,10,(3,4))
C =  matrixMut(A,B)
C1 = np.dot(np.mat(A),np.mat(B))
C
[[24, 84, 84, 115], [46, 56, 96, 80], [43, 112, 127, 152], [37, 84, 101, 119]]
C1
matrix([[ 24,  84,  84, 115],
        [ 46,  56,  96,  80],
        [ 43, 112, 127, 152],
        [ 37,  84, 101, 119]])

上面是矩阵相乘的程序,并且用numpy的矩阵相乘函数np.dot()检验。
我们发现对于一个矩阵相乘例如:&lt;A1,A2&gt;&lt;A_{1}, A_{2}&gt;<A1,A2>,他们的规模分别为10×100、 100*5.那么我们要做的标量乘法就是10⋅100⋅510\cdot 100 \cdot 5101005次。
对于一个矩阵链乘,会有不同的括号化方案,对应的不同的标量乘法次数。我们要找到一种括号化方案,似的矩阵的链乘积的标量乘法次数最少。

利用暴力法分析问题规模。

如果利用暴力法,那么我们就需要计算所有的括号化方案,来找到一个标量乘法次数最少的一个括号化方案,那么用P(n)来表示,那矩阵链的长度为n时,的
括号化的方案数。那么我们就有如下的递归方程。
P(n)={1if&ThickSpace;n==1∑k=1n−1P(k)P(n−k)if&ThickSpace;n≥2 P(n) = \left\{\begin{matrix} 1 &amp; if\; n==1 \\ \sum ^{n-1}_{k=1}P(k)P(n-k) &amp; if\; n\geq 2 \end{matrix}\right. P(n)={1k=1n1P(k)P(nk)ifn==1ifn2
这个递归式的结果为ω(2n)\omega (2^n)ω(2n),这是一种糟糕的策略。

应用动态规划的方法

利用我们一开头就提到的应用动态规划的步骤。

  1. 刻画一个最优的子结构
  2. 构造递归式
  3. 计算最有解通常使用自底向上的方法
  4. (如必要)根据计算的信息构造一个最优解。
第一步那就是找到一个最优的子结构了。

对于AiAi+1...AjA_{i}A_{i+1}...A{j}AiAi+1...Aj,对其进行括号化,加入我们在矩阵AkA_{k}Ak的后边添加一个")"那么我们下一步就要计算前边半条矩阵链相乘得到矩阵Ai,kA_{i,k}Ai,k的代价,加上后边矩阵链相乘得到矩阵Ak+1,jA_{k+1,j}Ak+1,j的代价,然后加上这两个矩阵相乘的代价,这就是全部的代价。
那么最优子结构就是对于AiAi+1...AjA_{i}A_{i+1}...A{j}AiAi+1...Aj,找到他的最优去括号化方案,将其分成两段,其中前段是AiAi+1...AkA_{i}A_{i+1}...A{k}AiAi+1...Ak,那么单独求这半个的链的最优括号化方案,如果将这个方案放在整个链中也是最优的括号化方案。这个原因可以用剪切–粘贴技术证明:

假如我们有一个矩阵链AiAi+1...AjA_{i}A_{i+1}...A{j}AiAi+1...Aj:它的最优括号化方案的分割点在k
方案一: 将其分成两段,AiAi+1...Ak,Ak+1Ak+2...AjA_{i}A_{i+1}...A{k},A_{k+1}A_{k+2}...A{j}AiAi+1...Ak,Ak+1Ak+2...Aj,我们分别单独求其的最优括号化方案,得到了:

AiAi+1...AkA_{i}A_{i+1}...A{k}AiAi+1...Ak, Ak+1Ak+2...AjA_{k+1}A_{k+2}...A{j}Ak+1Ak+2...Aj (这里我们用不同的颜色来表示最优的括号化方案。)
将这两段拼接起来就是整体链的最佳括号化方案。

方案二:如果我们直接求这个整体链的最佳括号化方法,得到如下的最佳方案。

AiAi+1...AkA_{i}A_{i+1}...A{k}AiAi+1...AkAk+1Ak+2...AjA_{k+1}A_{k+2}...A{j}Ak+1Ak+2...Aj

接下来我们将这个方案二的紫色的前一段AiAi+1...AkA_{i}A_{i+1}...A{k}AiAi+1...Ak提取出来,现在我们假设这一段不是AiAi+1...AkA_{i}A_{i+1}...A{k}AiAi+1...Ak的最优解,也就是说AiAi+1...AkA_{i}A_{i+1}...A{k}AiAi+1...Ak这个方案,比方案AiAi+1...AkA_{i}A_{i+1}...A{k}AiAi+1...Ak,代价更高。那么我们将AiAi+1...AkA_{i}A_{i+1}...A{k}AiAi+1...Ak这一段添加到方案二中得到:
AiAi+1...AkA_{i}A_{i+1}...A{k}AiAi+1...AkAk+1Ak+2...AjA_{k+1}A_{k+2}...A{j}Ak+1Ak+2...Aj
显然这个是更优的解,所以就证明了方案二是不成立的。

构造递归式

用m[i,j]来表示计算矩阵Ai..jA_{i..j}Ai..j所需要的最小标量乘法。
对于i==j的情况只有一个矩阵必然有m[i,j]=0,而对于i<j的情况,假设最佳分割点在矩阵AkA_{k}AkAk+1A_{k+1}Ak+1之间,那么我们就可以写出如下的递归式:
m[i,j]=m[i,k]+m[k+1,j]+pi−1pkpj m[i,j] = m[i,k] + m[k+1,j] + p_{i-1}p{k}p{j} m[i,j]=m[i,k]+m[k+1,j]+pi1pkpj
而实际中我们不知道k是多少,这里k的取值一共有j-i种可能,即k=i,i+1,...,jk=i,i+1,...,jk=i,i+1,...,j,我们遍历这些情况找到最佳的k值。所以递归式为:
m[i,j]={0if&ThickSpace;i==jmini≤k&lt;j{m[i,k],m[k+1,j]+pi−1pkpj}if&ThickSpace;i&lt;j m[i,j] = \left\{\begin{matrix} 0 &amp; if\; i==j\\ \underset {i\leq k &lt; j} {min} \{ m[i,k], m[k+1,j] + p_{i-1}p_{k}p_{j}\} &amp; if\; i &lt; j \end{matrix}\right. m[i,j]={0ik<jmin{m[i,k],m[k+1,j]+pi1pkpj}ifi==jifi<j

计算最优代价

假定矩阵AiA_{i}Ai的规模是pi−1×pi,&ThickSpace;(i=1,2,...,n)p_{i-1} \times p{i},\;(i=1,2,...,n)pi1×pi,(i=1,2,...,n),他的输入是一个维度序列,&lt;p0,p1,...pn&gt;&lt;p_{0},p_{1},...p_{n}&gt;<p0,p1,...pn>,这个过程用一个辅助表m[i,j]保存计算代价,而用s[1,…n-1,2…n]记录切割位置,以此来构造最优解。
为了采用自底向上的方法,我们必须确定计算m[i,j],会用到那些子问题,j-i+1个链相乘只依赖于那些比它短的链相乘的代价。
例如计算A1,3A_{1,3}A1,3的代价,他的长度为3,我们会用到链中所有长度小于3的链乘积代价,那么计算A1,4A_{1,4}A1,4,我们就会用到所有长度小于4的链的乘积代价,我们可以发现,我们可能会计算相同的子问题,这种子问题的重叠性也是动态规划的另一个标识。
那么我们采用自底向上的方法,我先解决规模最小的子问题,当我们需要解规模较大的子问题时,它所以依赖的较小子问题我们都已经解出来了,直接调用即可。所以我们就应该按照链的长度的递增顺序解决这个问题。

# 矩阵链乘积
def matrixChainOrder(p):
    n = len(p)-1
    m = [[0]*(n) for _ in range(n)]
    s = [[0]*(n-1) for _ in range(n-1)]
    for l in range(1,n):
        for i in range(n-l):
            j = i + l
            m[i][j] = float('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
    
p = [30,35,15,5,10,20,25]
m,s = matrixChainOrder(p)
m,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]])
最后构造最优解

目前我们可以求出一个矩阵链乘的最小代价,但是我们还不能显示出最优的括号化方案,求得的s,s[i,j]保存的就是从矩阵AiA_{i}Ai到矩阵AjA_{j}Aj加入“)”的位置。我们可以递归的加入"()"

def printOptParens(s,i,j):
    if i==j:
        print("A{}".format(str(i+1)),end="")
    else:
        print("(",end="")
        printOptParens(s,i,s[i][j-1]-1)
        printOptParens(s,s[i][j-1],j)
        print(")",end="")
printOptParens(s,0,5)
((A1(A2A3))((A4A5)A6))

1,对矩阵规模序列<5,10,3,12,5,50,6>,求矩阵链最优括号化方案

p = [5,10,3,12,5,50,6]
m,c = matrixChainOrder(p)
m,c
([[0, 150, 330, 405, 1655, 2010],
  [0, 0, 360, 330, 2430, 1950],
  [0, 0, 0, 180, 930, 1770],
  [0, 0, 0, 0, 3000, 1860],
  [0, 0, 0, 0, 0, 1500],
  [0, 0, 0, 0, 0, 0]],
 [[1, 2, 2, 4, 2],
  [0, 2, 2, 2, 2],
  [0, 0, 3, 4, 4],
  [0, 0, 0, 4, 4],
  [0, 0, 0, 0, 5]])
printOptParens(c,0,5)
((A1A2)((A3A4)(A5A6)))
  1. 设计递归算法MATRIX-CHAIN-MULTIPLY(A,s,i,j),实现矩阵链乘的算法。
def matrixChainMut(A,s,i,j):
    if i==j:
        return A[i]
    if i==j+1:
        return matrixMut(A[i],A[j])
    else:
        B1 = matrixChainMut(A,s,i,s[i][j-1]-1)
        B2 = matrixChainMut(A,s,s[i][j-1],j)
        return matrixMut(B1,B2)
# 根据维度链生成一系列随机矩阵
def randomMatrixs(p):
    matrixs = list()
    for i in range(len(p)-1):
        matrixs.append(np.random.randint(0,10,(p[i],p[i+1])))
    return matrixs
matrixs = randomMatrixs(p)
matrixChainMut(matrixs,s,0,5)
[[685009380, 681729124, 741674364, 597968918, 622221146, 737326806],
 [574114480, 571367532, 621146920, 500886252, 521416180, 617575464],
 [741957590, 738384282, 803052608, 647465550, 673855082, 798375320],
 [920582443, 916179757, 996816790, 803671532, 836228448, 990964623],
 [959841592, 955230604, 1038517360, 837402704, 871703472, 1032528964]]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值