文章目录
- 1.矩阵连乘问题描述
- 分治法
- 动态规划
- 1.自顶向下的动态规划(备忘录法)
- 2.自底向上的非递归动态规划
1.矩阵连乘问题描述
给定n个矩阵{A1,A2,…,An},其中Ai与Ai+1是可乘的,i=1,2…,n-1。如何确定计算矩阵连乘积的计算次序,使得依此次序计算矩阵连乘积需要的相乘次数最少 。
由于矩阵乘法满足结合律,所以可以有许多的不同的计算次序,然而不同的计算次序,相乘次数可能相差很大
具体例子:
有三个矩阵A1,A2,A3 大小分别为10* 100,100* 5,5*50,考虑**(A1(A2A3), (A1A2)A3 ** 的相乘次数
这里给出两个矩阵相乘所需数乘的公式
如下图
可以得到
- (A1(A2A3) 的次数为 7500
- (A1A2)A3 的次数为 75000
由此可见不同的次序,结果差距可能非常大
那么我们该如何找到最佳乘积次序呢
先引入以下符号:
- n表示矩阵的个数
- Ai表示第i个矩阵
- A[i : j]表示矩阵连乘AiAi+1…Aj
- pi表示Ai的列数
- pi-1表示Ai的行数
- s[i][j]表示矩阵连乘断开的位置断点k , 指在Ak和Ak+1之间断开
- m[i][j]表示A[i : j]的最小乘次
由矩阵相乘的条件可知: 前一个矩阵的列数 = 后一个矩阵的行数 。因此, pi既是Ai的列数,也是Ai+1的行数。
如下图
该问题有一个关键特征:计算矩阵链A[i :j]的最优计算方式,包含了子矩阵链A[i: k]和A[k+1 : j]的最优计算方式。
方法一*
分治法
对于每一个i,j区间,有一个变量k表示从第k个数组划分,k的范围是[i,j-1],枚举一个区间的所有划分情况然后求最小值就可以了,当然,由于我们是从上向下枚举的,比如划分A[1,4]为A[1:1],A[2,4]的情况,但是这两个子情况我们还不知道,所以就要再次递归,求解每一种子情况的答案
如图
如果我们把A[i : j]拆分成A[i : k]与A[k+1 : j],那么A[i : k]与A[k+1 : j]相乘的次数为多少?
A[i : k]的结果为pi-1* pk的矩阵,A[k+1 : j]的结果为pk* pj的矩阵,则它们的相乘的计算量为pi-1* pk * pj .
那我们该如何计算m[i][j](A[i : j]的最小乘次)呢?
- 如果 i=j ,则m[i][j]=0(只有一个矩阵)
- 如果i<j,则用加括号的方式将乘积AiAi+1…Aj分裂为AiAi+1…Ak和Ak+1Ak+2…Aj,其中 i<=k<j ,则m[i ,j]等于子乘积Ai…k和Ak+1…j的最小开销再加上这两个结果矩阵相乘的开销
则可得递归公式
k的位置有 j-i 种可能
private static int RecurMatrixChain(int i, int j, int[] p, int[][] s) {
// TODO Auto-generated method stub
if(i==j) {
return 0;
}
//取第一个断开位置时计算量为初始值
int m=RecurMatrixChain(i, i, p,s)+RecurMatrixChain(i+1, j, p,s)+p[i-1]*
p[i]*p[j];
//计断点为i
s[i][j]=i;
for (int k = i+1; k < j; k++) {
int temp=RecurMatrixChain(i, k, p,s)+RecurMatrixChain(k+1,j, p,s)+p[i-1]*
p[k]*p[j];
//取最小的计算量和当前的断点位置k
if(temp<m) {
m=temp;
s[i][j]=k;
}
}
return m;
}
求得最小计算量后,如何把乘积次序表示出来呢,这里我们用一个TraceBack方法通过递归找到断点 并加括号
由于s[i : j]是Ai…Aj的最优分裂位置,通过这个位置s[i : j],我们考虑这两个子问题Ai…As[i: j]和As[i: j]+1…Aj,而s[i, s[ i: j] ]记录问题Ai…As[i: j]的最优分裂位置,s[s[i: j]+1, j]记录问题As[i: j]+1…Aj的最优分裂位置
s[1][6]的求解步骤根据s表如下图:
private static void Traceback(int[][] s, int i, int j) {
if(i==j) {
System.out.print("A"+i);
}
else {
System.out.print("(");
Traceback(s, i, s[i][j]);
Traceback(s,s[i][j]+1,j);
System.out.print(")");
}
}
方法二
动态规划
1.首先看动态规划的基本步骤
- 找出最优解的性质,刻画其特征结构
- 递归的定义最优质(写出递归动态规划方程)
- 自顶向下或者自底向上的方式求出最优值
- 根据最优值求最优解
之前我们说过:
该问题有一个关键特征:计算矩阵链A[i :j]的最优计算方式,包含了子矩阵链A[i: k]和A[k+1 : j]的最优计算方式。
如果原问题的最优解,包含了其子问题的最优解,则我们称这种性质为最优子结构性质。若问题具有最优子结构性质,则可用动态规划算法求解。
1.自顶向下的动态规划(备忘录法)
在递归的基础上,创建一个二维数组存已经计算过的值
private static int MatrixChain(int i, int j, int[][] m, int[][] s, int[] p) {
//如果二维数组中已经存在我们要找的值则返回
if (m[i][j] != 0) {
return m[i][j];
} else {
if (i == j) {
m[i][j] = 0;
}
if (j > i) {
//取第一个断开位置时计算量为初始值
m[i][j]=MatrixChain(i, i, m, s,p)+MatrixChain(i+1, j, m, s, p)+p[i-1]*
p[i]*p[j];
s[i][j]=i;
for (int k = i+1; k < j; k++) {
int t=MatrixChain(i, k, m, s,p)+MatrixChain(k+1, j, m, s, p)+p[i-1]*
p[k]*p[j];
if(t<m[i][j]) {
m[i][j]=t;
s[i][j]=k;
}
}
}
}
return m[i][j];
}
2.自底向上的非递归动态规划
自底向上的计算过程中,保存已解决的子问题的答案。每个子问题只计算一次,而在后面只需要简单查一下,从而避免大量的重复计算。
如图
自底求出子问题,最后向上合并出我们想要解决的问题
为了正确实现自底向上方法,我们必须决定m表中哪些元素用于计算m[i,j],我们可以按照矩阵链长度增加的方式依次填充表格
如下图
这上面其实还有一行m[1][1],m[2][2],m[3][3]…m[n][n],因为它们都等于0,所以并未标注。
private static void MatrixChain(int m[][], int n, int s[][], int p[]) {
for (int i = 1; i <= n; i++) {
m[i][i] = 0;// 第一行的值均为0
}
// 依次从链长为[2:n]递增分别计算不同链长的矩阵连乘最优值
for (int l = 2; l <= n; l++) {
// 当计算链长为r时,i的取值范围
for (int i = 1; i <= n - l + 1; i++) {
// j与i的关系
int j = i + l - 1;
//先令m[i][j]最优值为矩阵链首元素划分下的值,后再进行对比
m[i][j] = m[i][i] + m[i + 1][j] + p[i - 1] * p[i] * p[j];
s[i][j] = i;
//计算k∈[i+1:j-1]递增划分下最优值,并和之前因保存的进行比较
for (int k = i + 1; k < j; k++) {
int temp = m[i][k] + m[k + 1][j] + p[i - 1] * p[k] * p[j];
if (temp < m[i][j]) {
m[i][j] = temp;
s[i][j] = k;
}
}
}
}
System.out.println(m[1][n]);
}