用动态规划求解矩阵链相乘的最少乘积次数的问题(C++实现)

用动态规划求解矩阵链相乘的最少乘积次数的问题(C++实现)

文章目录

  • 用动态规划求解矩阵链相乘的最少乘积次数的问题(C++实现)
    • *矩阵链相乘* 的问题描述
    • 问题分析
    • 算法设计
    • C++实现

矩阵链相乘 的问题描述

我们就不具体阐述这个问题的定义了。为了能够更加直白地让读者了解什么是矩阵链相乘的最小乘积次数问题,我们举个简单的例子:
假设有三个矩阵,分别是
M 1 M_1 M1
( 1 1 1 1 1 1 1 1 1 1 ) \begin{pmatrix} 1 & 1 \\ 1 & 1 \\ 1 & 1 \\ 1 & 1 \\ 1 & 1 \\ \end{pmatrix} 1111111111

M 2 M_2 M2
( 1 1 1 1 1 1 1 1 ) \begin{pmatrix} 1 & 1 & 1 & 1\\ 1 & 1 & 1 & 1\\ \end{pmatrix} (11111111)

M 3 M_3 M3
( 1 1 1 1 1 1 1 1 1 1 1 1 ) \begin{pmatrix} 1 & 1 & 1 \\ 1 & 1 & 1 \\ 1 & 1 & 1 \\ 1 & 1 & 1 \\ \end{pmatrix} 111111111111
这个问题就是要找到一个分割方式,使得矩阵相乘 M 1 ∗ M 2 ∗ . . . ∗ M n M_1*M_2*...*M_n M1M2...Mn最后在乘法上计算的次数最少。
比如我们这个问题, M 1 M_1 M1是一个 5 ∗ 2 5*2 52(5行2列)的矩阵, M 2 M_2 M2是一个 2 ∗ 4 2*4 24的矩阵, M 3 M_3 M3是一个 4 ∗ 3 4*3 43的矩阵,其实这个问题和矩阵的元素无关,只和矩阵的行数和列数相关,因此,上边的矩阵的元素我都填的1,没有什么影响。3个矩阵相乘有两种乘法,即

  1. ( M 1 M 2 ) M 3 (M_1M_2)M_3 (M1M2)M3,即先前两个矩阵相乘,然后再乘以第三个矩阵;
  2. M 1 ( M 2 M 3 ) M_1(M_2M_3) M1(M2M3),即先后两个矩阵相乘,然后再和第一个矩阵相乘。
  • 对于第1种情况, M 1 M_1 M1乘以 M 2 M_2 M2需要进行 5 ∗ 2 ∗ 4 = 40 5*2*4=40 524=40 次乘法运算,得到一个5行4列的矩阵 M M M,然后 M M M M 3 M_3 M3相乘需要进行 5 ∗ 4 ∗ 3 = 60 5*4*3=60 543=60次乘法运算,所以这种情况总共需要 40 + 60 = 100 40+60=100 40+60=100次乘法运算;
  • 对于第2种情况, M 2 M_2 M2乘以 M 3 M_3 M3需要进行 2 ∗ 4 ∗ 3 = 24 2*4*3=24 243=24 次乘法运算,得到一个2行3列的矩阵 M M M,然后 M 1 M_1 M1 M M M相乘需要进行 5 ∗ 2 ∗ 3 = 30 5*2*3=30 523=30次乘法运算,所以这种情况总共需要 24 + 30 = 54 24+30=54 24+30=54次乘法运算;

显然,第二种分割法乘法的运算次数较小,此时乘法次数为54。
我们需要做的就是,针对输入的矩阵的情况,求出这样的一个最少的乘积次数,同时能够给出分割的方法。

问题分析

对于乘积序列 M i M i + 1 . . . M j M_iM_{i+1}...M_j MiMi+1...Mj,我们假设使得其乘积最少的次数是C[i,j]。
k ∈ ( i , j ] k \in (i,j] k(i,j],我们将上边的序列在k的位置分割开,即得到 ( M i M i + 1 . . . M k − 1 ) ( M k . . . M j ) (M_iM_{i+1}...M_{k-1})(M_k...M_j) (MiMi+1...Mk1)(Mk...Mj)
此时,

  • 对于前面一个序列 ( M i M i + 1 . . . M k − 1 ) (M_iM_{i+1}...M_{k-1}) (MiMi+1...Mk1),其乘积的最少次数为C[i,k-1];对于后边一个序列 ( M k . . . M j ) (M_k...M_j) (Mk...Mj),其乘积的最少次数为C[k,j]。
  • 前一个序列的结果是一个 r i ∗ r k r_i*r_k rirk的矩阵 N 1 N_1 N1(其中, r i r_i ri表示 M i M_i Mi的行数, r k r_k rk表示 M k M_k Mk的行数,也就是 M k − 1 M_{k-1} Mk1的列数);后一个序列的结果是一个 r k ∗ r j + 1 r_k*r_{j+1} rkrj+1的矩阵 N 2 N_2 N2(其中, r k r_k rk表示 M k M_k Mk的行数, r j + 1 r_{j+1} rj+1表示 M j M_j Mj的列数);然后计算 N 1 N 2 N_1N_2 N1N2的乘积次数,其次数为 ( r i ∗ r k ∗ r j + 1 ) (r_i*r_k*r_{j+1}) (rirkrj+1)。所以,这样分割的情况下乘积的总次数为 n u m = ( C [ i , k − 1 ] + C [ k , j ] + ( r i ∗ r k ∗ r j + 1 ) ) num=(C[i,k-1]+C[k,j]+(r_i*r_k*r_{j+1})) num=(C[i,k1]+C[k,j]+(rirkrj+1))
  • 所以,经过以上分析我们有, C [ i , j ] = m i n { C [ i , k − 1 ] + C [ k , j ] + ( r i ∗ r k ∗ r j + 1 ) } C[i,j]=min\{C[i,k-1]+C[k,j]+(r_i*r_k*r_{j+1})\} C[i,j]=min{C[i,k1]+C[k,j]+(rirkrj+1)},其中 i < k ≤ j i \lt k \leq j i<kj (式1)

上式标黄的式子便是这个问题用动态规划方法求解的递推式子。

为了能够将这个式子正确地运用在程序中,我们用式(1)来分析一下博文开始的时候我们举的例子。
很明显,对于这个例子,我们的目标是计算出C[1,3]。而
C [ 1 , 3 ] = m i n { C [ 1 , 1 ] + C [ 2 , 3 ] + ( r 1 ∗ r 2 ∗ r 4 ) , C [ 1 , 2 ] + C [ 3 , 3 ] + ( r 1 ∗ r 3 ∗ r 4 ) } C[1,3]=min\{C[1,1]+C[2,3]+(r_1*r_2*r_4),C[1,2]+C[3,3]+(r_1*r_3*r_4)\} C[1,3]=min{C[1,1]+C[2,3]+(r1r2r4)C[1,2]+C[3,3]+(r1r3r4)}
C [ 1 , 2 ] = C [ 1 , 1 ] + C [ 2 , 2 ] + ( r 1 ∗ r 2 ∗ r 3 ) C[1,2]=C[1,1]+C[2,2]+(r_1*r_2*r_3) C[1,2]=C[1,1]+C[2,2]+(r1r2r3)
C [ 2 , 3 ] = C [ 2 , 2 ] + C [ 3 , 3 ] + ( r 2 ∗ r 3 ∗ r 4 ) C[2,3]=C[2,2]+C[3,3]+(r_2*r_3*r_4) C[2,3]=C[2,2]+C[3,3]+(r2r3r4)
同时,经过简单的分析,我们可以得到 C [ 1 , 1 ] = 0 C[1,1]=0 C[1,1]=0 C [ 2 , 2 ] = 0 C[2,2]=0 C[2,2]=0 C [ 3 , 3 ] = 0 C[3,3]=0 C[3,3]=0
也就是说,如果想要求 C [ 1 , 3 ] C[1,3] C[1,3],得先知道 C [ 1 , 2 ] C[1,2] C[1,2] C [ 2 , 3 ] C[2,3] C[2,3];如果想要求 C [ 1 , 2 ] C[1,2] C[1,2],得先知道 C [ 1 , 1 ] C[1,1] C[1,1] C [ 2 , 2 ] C[2,2] C[2,2];如果想要求 C [ 2 , 3 ] C[2,3] C[2,3],得先知道 C [ 2 , 2 ] C[2,2] C[2,2] C [ 3 , 3 ] C[3,3] C[3,3]。如果画一个表格的话,我们很容易地可以看到,这是一个先求斜对角线上的元素值的问题。

1 2 3
1 C[1,1] C[1,2] C[1,3]
2 C[2,2] C[2,3]
3 C[3,3]

对于上述的例子: r 1 = 5 , r 2 = 2 , r 3 = 4 , r 4 = 3 r_1=5,r_2=2,r_3=4,r_4=3 r1=5r2=2r3=4r4=3

  • C [ 1 , 1 ] = 0 C[1,1]=0 C[1,1]=0 C [ 2 , 2 ] = 0 C[2,2]=0 C[2,2]=0 C [ 3 , 3 ] = 0 C[3,3]=0 C[3,3]=0
  • C [ 1 , 2 ] = C [ 1 , 1 ] + C [ 2 , 2 ] + ( r 1 ∗ r 2 ∗ r 3 ) = 0 + 0 + 40 = 40 C[1,2]=C[1,1]+C[2,2]+(r_1*r_2*r_3)=0+0+40=40 C[1,2]=C[1,1]+C[2,2]+(r1r2r3)=0+0+40=40,此时K=2
  • C [ 2 , 3 ] = C [ 2 , 2 ] + C [ 3 , 3 ] + ( r 2 ∗ r 3 ∗ r 4 ) = 0 + 0 + 24 = 24 C[2,3]=C[2,2]+C[3,3]+(r_2*r_3*r_4)=0+0+24=24 C[2,3]=C[2,2]+C[3,3]+(r2r3r4)=0+0+24=24,此时K=3
  • C [ 1 , 3 ] = m i n { C [ 1 , 1 ] + C [ 2 , 3 ] + ( r 1 ∗ r 2 ∗ r 4 ) , C [ 1 , 2 ] + C [ 3 , 3 ] + ( r 1 ∗ r 3 ∗ r 4 ) } = m i n { 0 + 24 + 30 , 40 + 0 + 60 } = 54 C[1,3]=min\{C[1,1]+C[2,3]+(r_1*r_2*r_4),C[1,2]+C[3,3]+(r_1*r_3*r_4)\}=min\{0+24+30,40+0+60\}=54 C[1,3]=min{C[1,1]+C[2,3]+(r1r2r4)C[1,2]+C[3,3]+(r1r3r4)}=min{0+24+3040+0+60}=54,此时K=2,即最优的划分是 M 1 ( M 2 M 3 ) M_1(M_2M_3) M1(M2M3)

至此,我们简单地整理了一下计算的步骤,接下来就是很好写算法了。

算法设计

for i in 1...n:
    C[i,i]=0;
for d in 1...n-1:
	for i in 1...n-d:
		j=i+d;
		for k in i+1...j
			C[i,j]=min{C[i,k-1]+C[k,j]+(r_i*r_k*r_(j+1))}
print C[1,n]

C++实现

//动态规划求解矩阵链相乘的最少乘积次数问题
//输入:矩阵的个数N,以及每一个矩阵的行数l和列数c
//输出:最少乘积次数,以及对应的乘积顺序
/*
示例输入:
5
4 10
10 5
5 6
6 3
3 10

示例输出:
i=1,j=2:k= 2
i=2,j=3:k= 3
i=3,j=4:k= 4
i=4,j=5:k= 5
i=1,j=3:k= 3
i=2,j=4:k= 3
i=3,j=5:k= 5
i=1,j=4:k= 3
i=2,j=5:k= 5
i=1,j=5:k= 5
result: 470
*/

#include
#include

using namespace std;
const int MAX_N = 100; //假设输入的矩阵的个数小于100个
const int MAX_INT = 65530; //假设最后的结果不会大于MAX_INT值
int C[MAX_N][MAX_N];   //C[i][j]的意思是,从第i个矩阵乘到第j个矩阵需要的最少乘积次数
int Index_K[MAX_N][MAX_N];
/*Index_K[i][j]与C[i][j]对应,意思是 先做第i个矩阵乘到第Index_K[i][j]个矩阵,然后做第Index_K[i][j]个矩阵乘到第j个矩阵,接着将这两个结果矩阵相乘,这样的分法可以使C[i][j]最小*/
struct matrix
{
    int m_l, m_c;
};
matrix mats[MAX_N];   //保存输入的矩阵
int R[MAX_N];  //R[i]保存输入的矩阵i的行数,R[N+1]保存输入的矩阵N的列数
int N;    //输入矩阵的个数

bool isMatricesLegal();  //判断输入的矩阵是否可以做乘法运算

int main()
{
    cin>>N;
    int l,c;   //输入矩阵的行数和列数
    for(int i=1;i<=N;i++)
    {
        cin>>l>>c;
        mats[i].m_l=l;
        mats[i].m_c=c;
    }
    //这里可以判断一下输入的矩阵行列值是否符合运算的要求
    if(!isMatricesLegal()) return 1;
    for(int i=1;i<=N;i++) R[i]=mats[i].m_l;
    R[N+1]=mats[N].m_c;
    for(int i=1;i<=N;i++)
    {
        C[i][i]=0;   //无需做乘法运算
    }
    for(int d=1;d<=N-1;d++)
    {
        for(int i=1;i<=N-d;i++)
        {
            int j=i+d;
            int min_tmp=MAX_INT;
            for(int k=i+1;k<=j;k++)
            {
                if(min_tmp>(C[i][k-1]+C[k][j]+R[i]*R[k]*R[j+1]))
                {
                    min_tmp=C[i][k-1]+C[k][j]+R[i]*R[k]*R[j+1];
                    Index_K[i][j]=k;
                }
                //min_tmp = min(min_tmp,C[i][k-1]+C[k][j]+R[i]*R[k]*R[j+1]);
            }
            C[i][j]=min_tmp;
            cout<<"i="<<i<<",j="<<j<<":"<<"k= "<<Index_K[i][j]<<endl;
        }
    }
    cout<<"result:\t"<<C[1][N]<<endl;   //从矩阵1乘到矩阵N的最少的乘积次数

    return 0;
}

bool isMatricesLegal()
{
    for(int i=1;i<N;i++)
    {
        if(mats[i].m_c!=mats[i+1].m_l) return false;  //如果前一个矩阵的列数不等于后一个矩阵的行数,则不能进行乘法运算
    }
    return true;
}

对于代码中注释的输入例子,我们照样可以手动计算一下对应的表格,该表格内容如下:

1 2 3 4 5
1 0 200 320 350 470
2 0 300 240 540
3 0 90 240
4 0 180
5 0

对应的最优划分为 ( ( M 1 M 2 ) ( M 3 M 4 ) ) ( M 5 ) ((M_1M_2)(M_3M_4))(M_5) ((M1M2)(M3M4))(M5)

你可能感兴趣的:(算法与程序)