算法设计与分析——动态规划(三):矩阵链乘法

分类目录:《算法设计与分析》总目录
相关文章:
· 动态规划(一):基础知识
· 动态规划(二):钢条切割
· 动态规划(三):矩阵链乘法
· 动态规划(四):动态规划详解
· 动态规划(五):最长公共子序列


这个例子是求解矩阵链相乘问题的动态规划算法。给定一个n个矩阵的序列(矩阵链) ( A 1 . A 2 . ⋯   , A n ) (A_1. A_2. \cdots, A_n) (A1.A2.,An),我们希望计算它们的乘积 A 1 A 2 ⋯ A n A_1A_2\cdots A_n A1A2An其中,我们可以先用括号明确计算次序,然后利用标准的矩阵相乘算法进行计算。由于矩阵乘法满足结合律,因此任何加括号的方法都会得到相同的计算结果。我们称有如下性质的矩阵乘积链为完全括号化的:它是单一矩阵,或者是两个完全括号化的矩阵乘积链的积,且已外加括号。例如,如果矩阵链为 ( A 1 , A 2 , A 3 ) (A_1, A_2, A_3) (A1,A2,A3),则共有2种完全括号化的矩阵乘积链: ( ( A 1 A 2 ) A 3 ) ((A_1A_2)A_3) ((A1A2)A3) ( A 1 ( A 2 A 3 ) ) (A_1(A_2A_3)) (A1(A2A3))

对矩阵链加括号的方式会对乘积运算的代价产生巨大影响。我们先来分析两个矩阵相乘的代价:

def matrix_multiply(A:np.array, B:np.array):
    if A.shape[1] != B.shape[0]:
        print('Incompatible Dimensions')
    else:
        ans = np.zeros([A.shape[0], B.shape[1]])
        for row_index in range(len(A)):
            for column_index in range(len(B[0])):
                ans[row_index, column_index] = sum(A[row_index] * B[:, column_index])
    return ans

两个矩阵 A A A B B B只有相容,即 A A A的列数等于 B B B的行数时,才能相乘。如果 A A A p × q p×q p×q的矩阵, B B B q × r q×r q×r的矩阵,那么乘积 C C C p × r p×r p×r的矩阵。计算 C C C所需时间由 p q r pqr pqr决定。

我们以矩阵链 ( A 1 , A 2 , A 3 ) (A_1, A_2, A_3) (A1,A2,A3)相乘为例,来说明不同的加括号方式会导致不同的计算代价假设三个矩阵的规模分别为 10 × 100 10×100 10×100 100 × 5 100×5 100×5 5 × 50 5×50 5×50。如果按 ( ( A 1 A 2 ) A 3 ) ((A_1A_2)A_3) ((A1A2)A3)的顺序计算,为计算 A 1 A 2 A_1A_2 A1A2,需要做 10 × 100 × 5 = 5000 10×100×5=5000 10×100×5=5000次标量乘法,再与 A 3 A_3 A3相乘又需要做 10 × 5 × 50 = 2500 10×5×50=2500 10×5×50=2500 次标量乘法,共需 7500 7500 7500次标量乘法。如果按 ( A 1 ( A 2 A 3 ) ) (A_1(A_2A_3)) (A1(A2A3))的顺序,计算 A 2 A 3 A_2A_3 A2A3,需 100 × 5 × 50 = 25000 100×5×50=25000 100×5×50=25000次标量乘法, A 1 A_1 A1再与之相乘又需 10 × 100 × 50 = 50000 10×100×50=50000 10×100×50=50000次标量乘法共需 75000 75000 75000次标量乘法。因此,按第一种顺序计算矩阵链乘积要比第二种顺序快10倍。

所以,矩阵链乘法问题可描述如下:给定 n n n个矩阵的链) ( A 1 . A 2 . ⋯   , A n ) (A_1. A_2. \cdots, A_n) (A1.A2.,An),矩阵 A i A_i Ai的规模为 p i − 1 × p i p_{i-1}×p_i pi1×pi,求完全括号化方案,使得计算乘积 A 1 A 2 ⋯ A n A_1A_2\cdots A_n A1A2An所需标量乘法次数最少。

注意,求解矩阵链乘法问题并不是要真正进行矩阵相乘运算,我们的目标只是确定代价最低的计算顺序。确定最优计算顺序所花费的时间通常要比随后真正进行矩阵相乘所节省的时间要少。

在用动态规划方法求解矩阵链乘法问题之前,我们先来证明穷举所有可能的括号化方案不会产生一个高效的算法。对一个 n n n个矩阵的链,令 P ( n ) P(n) P(n)表示可供选择的括号化方案的数量。当 n = 1 n=1 n=1时,由于只有一个矩阵,因此只有一种完全括号化方案。当 n ≥ 2 n≥2 n2时,完全括号化的矩阵乘积可描述为两个完全括号化的部分积相乘的形式,而两个部分积的划分点在第 k k k个矩阵和第 k + 1 k+1 k+1个矩阵之间, k k k 1 , 2 , ⋯   , n − 1 1, 2, \cdots, n-1 1,2,,n1中的任意一个值。因此,当 n ≥ 2 n≥2 n2时我们可以得到如下递归公式:
p ( n ) = ∑ k = 1 n − 1 p ( k ) P ( n − k ) p(n)=\sum_{k=1}^{n-1}p(k)P(n-k) p(n)=k=1n1p(k)P(nk)

因此,括号化方案的数量与 n n n呈指数关系,通过暴力搜索穷尽所有可能的括号化方案来寻找最优方案,是个糟糕的策略。

应用动态规划方法

下面用动态规划方法来求解矩阵链的最优括号化方案,我们还是按照《算法设计与分析——动态规划(一):基础知识》开头提出的4个步骤进行:

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

我们按顺序进行这几个步骤,清楚地展示针对本问题每个步骤应如何做。

步骤1:最优括号化方案的结构特征

动态规划方法的第一步是寻找最优子结构,然后就可以利用这种子结构从子问题的最优解构造出原问题的最优解。在矩阵链乘法问题中,此步骤的做法如下所述。为方便起见,我们用符号 A i , j ( i ≤ j ) A_{i, j}(i\leq j) Ai,j(ij)表示 A i A i + 1 ⋯ A j A_iA_{i+1} \cdots A_j AiAi+1Aj乘积的结果矩阵。可以看出,如果问题是非平凡的,即 i ≤ j i\leq j ij,那么为了对 A i A i + 1 ⋯ A j A_iA_{i+1} \cdots A_j AiAi+1Aj进行括号化,我们就必须在某个 A k A_k Ak A k + 1 A_{k+1} Ak+1之间将矩阵链划分开。也就是说,对某个整数 k k k,我们首先计算矩阵 A i , k A_{i, k} Ai,k A k + 1 , j A_{k+1,j} Ak+1,j,然后再计算它们的乘积得到最终结果 A i , j A_{i, j} Ai,j。此方案的计算代价等于矩阵 A i , k A_{i, k} Ai,k的计算代价,加上矩阵 A k + 1 , j A_{k+1,j} Ak+1,j,的计算代价,再加上两者相乘的计算代价。

下面我们给出本问题的最优子结构。假设 A i A i + 1 ⋯ A j A_iA_{i+1} \cdots A_j AiAi+1Aj的最优括号化方案的分割点在 A k A_k Ak A k + 1 A_{k+1} Ak+1之间。那么,继续对“前缀”子链 A i , k A_{i, k} Ai,k进行括号化时,我们应该直接采用独立求解它时所得的最优方案。

我们已经看到,一个非平凡的矩阵链乘法问题实例的任何解都需要划分链,而任何最优解都是由子问题实例的最优解构成的。因此,为了构造一个矩阵链乘法问题实例的最优解,我们可以将问题划分为两个子问题 A i A i + 1 ⋯ A k A_iA_{i+1} \cdots A_k AiAi+1Ak A k + 1 A k + 2 ⋯ A j A_{k+1}A_{k+2} \cdots A_j Ak+1Ak+2Aj,的最优括号化问题,求出子问题实例的最优解,然后将子问题的最优解组合起来。我们必须保证在确定分割点时,已经考察了所有可能的划分点,这样就可以保证不会遗漏最优解。

步骤2:一个递归求解方案

下面用子问题的最优解来递归地定义原问题最优解的代价。令 m [ i , j ] m[i, j] m[i,j]表示计算矩阵 A i , j A_{i, j} Ai,j所需标量乘法次数的最小值,那么,原问题的最优解——计算 A i , n A_{i, n} Ai,n所需的最低代价就是 m [ 1 , n ] m[1, n] m[1,n]

我们可以递归定义 m [ i , j ] m[i, j] m[i,j]如下。对于 i = j i=j i=j时的平凡问题,矩阵链只包含唯一的矩阵 A i A_i Ai,因此不需要做任何标量乘法运算。所以,对所有 i = 1 , 2 , ⋯   , n i=1, 2, \cdots, n i=1,2,,n,有 m [ i , i ] = 0 m[i, i]=0 m[i,i]=0,若 i < j i<j i<j,我们利用步骤1中得到的最优子结构来计算 m [ i , j ] m[i, j] m[i,j]。我们假设 A i A i + 1 ⋯ A j A_iA_{i+1} \cdots A_j AiAi+1Aj的最优括号化方案的分割点在矩阵 A k A_k Ak A k + 1 A_{k+1} Ak+1之间。那么 m [ i , j ] m[i, j] m[i,j]就等于计算 A k A_k Ak A k + 1 A_{k+1} Ak+1的代价加上两者相乘的代价的最小值。由于矩阵 A i A_i Ai的规模为 p i − 1 × p i p_{i-1}×p_i pi1×pi,易知 A i , k A_{i, k} Ai,k A k + 1 , j A_{k+1, j} Ak+1,j相乘的代价为 p i − 1 p k p j p_{i-1}p_kp_j pi1pkpj次标量乘法运算。因此,我们得到

m [ i , j ] = m [ i , k ] + m [ k + 1 , j ] + p i − 1 p k p j ( i < j ) m[i, j]=m[i, k] + m[k + 1, j] + p_{i-1}p_kp_j (i<j) m[i,j]=m[i,k]+m[k+1,j]+pi1pkpj(i<j)

此递归公式假定最优分割点 k k k是已知的,但实际上我们是不知道的。不过, k k k只有 j − i j-i ji种可能的取值,即 k = i , i + 1 , ⋯ , j − 1 k=i, i+1, \cdots,j-1 k=i,i+1,j1,由于最优分割点必在其中,我们只需检查所有可能情况,找到最优者即可。

m [ i , j ] m[i, j] m[i,j]的值给出了子问题最优解的代价,但它并未提供足够的信息来构造最优解。为此,我们用 s [ i , i ] s[i, i] s[i,i]保存 A i A i + 1 ⋯ A j A_iA_{i+1} \cdots A_j AiAi+1Aj最优括号化方案的分割点位置 k k k,即使得 m [ i , j ] = m [ i , k ] + m [ k + 1 , j ] + p i − 1 p k p j ( i < j ) m[i, j]=m[i, k] + m[k + 1, j] + p_{i-1}p_kp_j (i<j) m[i,j]=m[i,k]+m[k+1,j]+pi1pkpj(i<j)成立的 k k k值。

步骤3:计算最优代价

现在,我们可以很容易地基于递归公式写出一个递归算法,来计算 A 1 A 2 ⋯ A n A_1A_2\cdots A_n A1A2An相乘的最小代价 m [ 1 , n i ] m[1, ni] m[1,ni]。像我们在《算法设计与分析——动态规划(二):钢条切割》中所看到的那样,此递归算法是指数时间的,并不比检查所有括号化方案的暴力搜索方法更好。

注意到,我们需要求解的不同子问题的数目是相对较少的:每对满足 1 ≤ i ≤ j ≤ n 1\leq i\leq j\leq n 1ijn i i i j j j对应一个唯一的子问题,共有 O ( 2 n ) O(2^n) O(2n)个。递归算法会在递归调用树的不同分支中多次遇到同一个子问题。这种子问题重叠的性质是应用动态规划的另一个 s [ i , j ] s[i, j] s[i,j]来标识( m [ i , j ] m[i, j] m[i,j]标识是最优子结构)。

我们采用自底向上表格法代替基于公式递归算法来计算最优代价。下面给出的过程 matrix_chain_order(p:list)实现了自底向上表格法。此过程假定矩阵 A i A_i Ai的规模为 p i − 1 × p i p_{i-1}×p_i pi1×pi。它的输入是一个序列 p 0 , p 1 , ⋯   , p n p_0, p_1, \cdots, p_n p0,p1,,pn,其长度为 n + 1 n+1 n+1,过程用一个辅助表 m [ i , j ] m[i, j] m[i,j]来保存,用另一个辅助表 s [ i , j ] s[i, j] s[i,j]记录最优值 m [ i , j ] m[i, j] m[i,j]对应的分割点 k k k。我们就可以利用表 s s s构造最优解。

def matrix_chain_order(p:list):
    m = np.array([[-1] * (len(p) - 1)] * (len(p) - 1))
    s = np.array([[-1] * (len(p) - 1)] * (len(p) - 1))
    
    for matrix_length in range(1, len(p)):
        for i in range(len(p) - matrix_length):
            if matrix_length == 1:
                m[i, i] = 0
                s[i, i] = i
            else:
                j = i + matrix_length - 1
                cost = 1e10
                for k in range(i,j):
                    _ = sum(m[i, k] + m[k + 1, j] + p[i - 1] * p[k] * p[j])
                    if _ < cost:
                        cost = _
                        m[i, j] = cost
                        s[i, j] = k
    return m, s

简单分析 matrix_chain_order(p:list)的嵌套循环结构,可以看到算法的运行时间为 O ( n 3 ) O(n^3) O(n3)。此外,算法还需要 O ( n 2 ) O(n^2) O(n2)的内存空间来保存表ms。因此, matrix_chain_order(p:list)比起穷举所有可能的括号化方案来寻找最优解的指数阶算法要高效得多。

步骤4:构造最优解

虽然 matrix_chain_order(p:list)求出了计算矩阵链乘积所需的最少标量乘法运算次数,但它并未直接指出如何进行这种最优代价的矩阵链乘法计算。表 s [ i , j ] s[i, j] s[i,j]记录了构造最优解所需的信息。每个表 s [ i , j ] s[i, j] s[i,j]记录了一个 k k k值,指出 A i A i + 1 ⋯ A j A_iA_{i + 1}\cdots A_j AiAi+1Aj的最优括号化方案的分割点应在 A k A_k Ak A k + 1 A_{k+1} Ak+1之间。因此,我们通过表 s [ i , j ] s[i, j] s[i,j]可以很轻易的得出整个矩阵链的切分点。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

von Neumann

您的赞赏是我创作最大的动力~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值