老规矩,我们先由题切入:

矩阵链乘法问题(matrix-chain multiplication problem)可描述如下:

给定n个矩阵的链<A1,A2,...,An>,其中,Ai和Ai+1是可乘的,( 矩阵Ai的规模为p(i-1)×p(i) (1<=i<=n) ),求完全括号化方案,使得计算乘积A1A2...An所需标量乘法次数最少。

即:

矩阵链乘法 Java 矩阵链乘法流程图_矩阵

题干描述就是如此,除此之外,我们必须清楚矩阵的几个性质:

1.矩阵链式乘法满足结合律

2.计算同一个矩阵链的乘法有很多种,虽然最后结果一样,但是所需乘法的代价是不一样的

3.A是p×q的矩阵,B是q*r的矩阵,那么矩阵C=A*B是p×r的矩阵。C所需的标量乘法的次数为pqr。

我们举个例子:

矩阵链乘法 Java 矩阵链乘法流程图_动态规划_02

接下来我们说解题思路:

首先定义m[i,j]:定义为矩阵Ai*A(i+1)*...*Aj所需乘法代价最小的次数。

所以:

矩阵链乘法 Java 矩阵链乘法流程图_算法_03

pi-1*pk*pj是计算到最后两个矩阵(i-1)*k 和矩阵k*j相乘所需的乘法代价。


c++代码如下:

#include <iostream>

using namespace std;
const int intmax=2147483647;
int const M=8; //M为存储矩阵边的数组的大小,M=n+1(n为矩阵个数)
int MatrixChainOrder(int *p,int Length,int m[][M],int s[][M]) //Length为存储边的数组的长度,n=Length-1为矩阵个数
{
	int q,n=Length-1;
	for(int i=1;i<=n;i++) m[i][i]=0; //初始化矩阵m[][]都为0

	for(int l=2;l<=n;l++) 	//l为要拆成的矩阵链的长度
	{
		for(int i=1;i<=n-l+1;i++)
		{
			int j=i+l-1; //为什么j=i+l-1呢,因为矩阵链长度最大为l,所以根据i我们可以得到j
			m[i][j]=intmax;  //m[i][j]初始值为0,因为后面要比较m[i][k]+m[k+1][j]+p[i-1]*p[k]*p[j] 和m[i][j]谁更小,所以我们在这里给m[i][j]赋一个很大的值
			for(int k=i;k<=j-1;k++)  //比较m[i][k]+m[k+1][j]+p[i-1]*p[k]*p[j] 和m[i][j]谁小
			{
				q=m[i][k]+m[k+1][j]+p[i-1]*p[k]*p[j];
				if(q<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]+p[i-1]*p[k]*p[j],并且将此时的k值储存在s[i][j]中
				{
					m[i][j]=q;
					s[i][j]=k;
				}
			}
		}
	}
return m[1][n]; //最后结果
}

void PrintPath(int s[][M],int i,int j)
{
	if(i == j) cout<<"A"<<i;
	else
	{
		cout<<"(";
		PrintPath(s,i,s[i][j]);
		PrintPath(s,s[i][j]+1,j);
		cout<<")";
	}
}

int main()
{

   int p[M]={30,35,15,5,10,20,25,10};//7个矩阵,但是存了8个边
   int m[M][M],s[M][M];
   int k=MatrixChainOrder(p,M,m,s);
   cout<<"当有7个矩阵时,最优解为: \n"<<k<<endl;
   PrintPath(s,1,7);

    return 0;
}

输出结果:

矩阵链乘法 Java 矩阵链乘法流程图_算法_04

代码详解:

涉及到两个函数:

1.(动态规划)MatrixChainOrder():得到最小乘法代价时的乘法的结合方式,并且求出最小乘法代价的数值

2.(递归)PrintPath():根据MatrixChainOrder()得到的s[i][j],得到输出乘法结合方式

代码讲解:

本代码涉及到的矩阵例子是:六个矩阵,如下图

矩阵链乘法 Java 矩阵链乘法流程图_c++_05

1.函数MatrixChainOrder():得到最小乘法代价时的乘法的结合方式,并且求出最小乘法代价的数值

输入数组:p[M ]:存储n个矩阵的M(即n+1)条边

数组s[i][j]:存储i和j之间的k值,最后我们要的是这个

代码中三层循环中第一层循环:l为矩阵链的长度 ,即将矩阵从两个两个拆分,然后在三个三个拆以此类推。

第二层循环:m[i][j]中的i从一开始循环,j=i+l-1, 为什么呢,因为矩阵链长度最大为l,所以根据i和l我们可以得到j=i+l-1

第三层循环:从i开始依次寻找断点k,然后比较m[i][k]+m[k+1][j]+p[i-1]*p[k]*p[j] 和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]+p[i-1]*p[k]*p[j],并且将此时的k值储存在s[i][j]中

我们根据图片可以得到更加直观的理解:

假设现在有一个包含七个矩阵的矩阵链:

那么m[i][j]的计算顺序如图所示:

矩阵链乘法 Java 矩阵链乘法流程图_c++_06

 2.(递归)PrintPath():根据MatrixChainOrder()函数得到的s[i][j],计算并输出乘法结合方式

由上文可知,s[i,j]记录了k值,指出A(i)A(i+1)...A(j)的最优括号化方案的分割点应在A(k)和A(k+1)之间。

A1..An的最优方案中最后一次矩阵乘法运算应该是以s[1,n]为分界的( A1*A2..*A(s[1,n])  )*( A(s[1,n]+1)*..*An

而s[1,s[1,n]]指出了计算A1*A2*..A(s[1,n])时应进行的最后一次矩阵乘法运行;s[s[1,n]+1,n]指出了计算A(s[1,n]+1)*..*An时应进行的最后一次矩阵乘法运算。

所以我们可以递归过程输出<A(i),A(i+1),...,A(j)>的最优括号化方案。