矩阵连乘问题的递归、自底向上、自顶向下(Java)
问题
给定n个矩阵{A1,A2,…,An},其中Ai与Ai+1是可乘的,i=1,2…,n-1。如何确定计算矩阵连乘积的计算次序,使得依此次序计算矩阵连乘积需要的数乘次数最少。例如,给定三个连乘矩阵{A1,A2,A3}的维数分别是10×100,100×5和5×50,采用(A1A2)A3,乘法次数为10×100×5+10×5×50=7500次,而采用A1(A2A3),乘法次数为100×5×50+10×100×50=75000次乘法,显然,最好的次序是(A1A2)A3,乘法次数为7500次。
分析
由上面的例子可见,相同的矩阵连乘用不同的顺序,其效率差距可能是巨大的,甚至是达到10倍以上。
首先我们先来看下两个矩阵相乘的数乘次数是如何计算的
简而言之,就是行1✖列1(行2)✖列2。
我们再看看多个矩阵
动态规划
我们发现有重复计算的矩阵连乘,那我们可以存储起来去查表。但是这是建立在每一步都是最优解的基础上去存储的,那么怎么求最优解呢?
-
寻找最优子结构
先看上课讲的PPT吧
此问题最难的地方便在于找到最优子结构。PPT中表达的意思是,若用动态规划方法,那么有一k使得Ai…Aj分裂(Ai——Ak,Ak+1——Aj),此时他们的两部分自身也一定都要求是最小的数乘次数(就是把两个划分部分看成是两个矩阵P、Q),且两部分再相乘为整个矩阵链的最小,那么k就是最佳括号位置。
可是k有这么多位置,我们怎么知道哪个位置最佳呢?最简单的方法,那我们就都试一遍啊!
-
建立递归关系
还是先上PPT
对于矩阵自身,是不可能相乘的,所以m[i,i]=0
根据分析1所知,i——j中存在某个划分位置k(假设已经确定),能使得Ai——Aj的数乘次数最小,值为两个部分的最小数乘次数相加再加上两个部分相乘的次数,即
m[i,j] = m[i,k] + m[k+1,j] + pi-1 ✖ pk ✖ pj
但是因为k有很多种选择(在i——j中有j-i种选择)而最优括号位置只有一个,那么到底是谁呢?
我们只需要逐个检查,然后两两对比,就能找出其中最小的m[i,j],这样就得到了最优位置。
因此矩阵链Ai——Aj的最小数乘递推式为
在计算得到最小m[i, j]的同时,也要把相应的k的位置放在s[i, j]中。
方法
普通递归方法
这种方法其实也是一种自顶向下的思想,从大问题出发,假设大问题的最优解,然后逐步向下到小问题,直到得到最小问题的最优解,就可以再还原到大问题的最优解了。
可以将一个矩阵链分解成这样的结构,由图可见,许多子问题被重复计算了很多次(圈出两个例子),这里就是我们可以用dp去改进的地方。
/*
* @Title recursiveMatrixMultiply
* @Description 普通递归算法 i:矩阵序列的左起始点 j:矩阵序列的右起始点
* @author 滑技工厂
* @Date 2020/4/22
* @param [i, j]
* @return int 计算出来是Ai——Aj这段矩阵序列的最小相乘次数
* @throws
*/
public static int recursiveMatrixMultiply(int i, int j) {
//表示最小值,用来与i<j时每次k划分的m[i,j]进行比较,若m[i,j]小则把其赋值给tempMin,表示目前他是最小
int tempMin = MAX;
if (i == j)
return 0;
//表示从k=i——j-1的所有划分情况,在这些情况中,每次k都要与上次比大小,找出最小的并付给tempMin
//不能等于j,因为上限是j,递归方程中有k+1---j的值
for (int k = i; k < j; k++) {
//k表示i--j中的分割位置,分割为i——k,k+1——j
int result = recursiveMatrixMultiply(i, k) + recursiveMatrixMultiply(k + 1, j) + p[i - 1] * p[k] * p[j];
//如果得到的结果比当前最小值小,则更新最小值,并更新s[i,j]的值,也就是划分位置k
if (result < tempMin) {
tempMin = result;
s[i][j] = k;
}
}
return tempMin;
}
自底向上的动态规划方法
与自顶向下有所不同,自底向上是首先去逐步求小问题的情况,然后在合并成更大一点点的,再合并,直到大问题。
直观一点就是下图。
这样通过不同的组合就可以得到整个矩阵链的最佳开销(如A1——A2与A3——An组合)
这里我们通过i与j的位置来控制小链的长度,小链先从1,然后是2,然后是3,直到n。i与j的差就是长度,每个长度是一层,因此要按照箭头的方向依次计算,直到一层完毕,再换链长。(另外,每一个m[i,j]的计算过程就是那个递推式的过程,依旧要循环找。)
/*
* @Title downToUpDPMatrixMultiply
* @Description 自底向上的dp解法
* @author 滑技工厂
* @Date 2020/4/24
* @param []
* @return void
* @throws
*/
public static void downToUpDPMatrixMultiply() {
//定义链长的最大长度
int n = p.length - 1;
//r为每次计算的矩阵的链长,2——n
for (int r = 2; r <= n; r++) {
//定义在每次链长r中,矩阵链中各个矩阵结合的小链左起点i 终止位置为n-r+1 j根据r+i-1来定
for (int i = 1; i <= n - r + 1; i++) {
int j = i + r - 1;
//m[i][j]为对应的r-1的小链的最优乘积次数值加上乘上矩阵[i]的次数
m[i][j] = m[i + 1][j] + p[i - 1] * p[i] * p[j];
//另外这里就代表k=i的划分
s[i][j] = i;
//每个小链里的划分情况,找出最小的那个
for (int k = i + 1; k < j; k++) {
int min = m[i][k] + m[k + 1][j] + p[i - 1] * p[k] * p[j];
if (min < m[i][j]) {
m[i][j] = min;
s[i][j] = k;
}
}
}
}
}
自顶向下的动态规划方法
这个与递归其实很像,差别就是在求m[i,j]的时候先查表,如果有则直接返回,没有则依照递推公式计算,此时计算出m[i,j]时,要存在表中,以便大问题去调用自己的结果,避免多次计算,节省效率。
/*
* @Title upToDownDPMatrixMultiply
* @Description 自顶向下的dp解法
* @author 滑技工厂
* @Date 2020/4/23
* @param [i, j]ij同上
* @return int
* @throws
*/
public static int upToDownDPMatrixMultiply(int i, int j) {
int tempMin = MAX;
if (i==j)
return 0;
//与递归算法不同的是,这里可以查表,对于已经计算过一次的m[i][j],可以直接查表获得对应的计算次数,而不用多次去计算m[i][j]
if (m[i][j]!=0)
return m[i][j];
for (int k = i; k < j; k++) {
int result = upToDownDPMatrixMultiply(i, k) + upToDownDPMatrixMultiply(k + 1, j) + p[i - 1] * p[k] * p[j];
if (result < tempMin) {
tempMin = result;
s[i][j] = k;
}
m[i][j] = tempMin;
}
return tempMin;
}
好啦,本篇博客到这里就结束了,如果你喜欢的话请给我一个👍,觉得我哪里有错误或者不理解也可以在评论区提出。你们的支持就是我最大的动力。
完整的代码请移步的我GitHub:滑技工厂——Homework-AlgorithmAnalysis,如果能给我一颗⭐⭐自然是极好的。
再见!
(每篇博客少不了的猫头)