1. 问题描述#
给定矩阵序列 A1,A2,…,An,矩阵相乘顺序不能改变,但可以改变加括号的方式。
目标:找到一种加括号方式,使矩阵连乘的总计算次数最少。
需要注意的是,矩阵连乘问题不是要求真正算出矩阵乘积,而是要求找到最优计算顺序。
2. 括号的位置会影响计算次数#
矩阵相乘有一个很容易被忽略的点:先算谁,会改变中间结果的规模。
对于两个矩阵 A(p×q) 和 B(q×r),它们相乘需要的计算次数是:
p×q×r而乘完之后,得到的新矩阵规模是:
p×r中间的 q 只负责“对齐”两个矩阵,真正留下来的只有外侧的 p 和 r:
(p×q)×(q×r)=p×r这就带来一个问题:如果有多个矩阵连乘,某一步产生的中间矩阵太大,后面的计算量也会被一起放大。
来看一个例子,维度数组为:
p=[2, 3, 6, 4, 2, 7]对应 5 个矩阵:
A1(2×3),A2(3×6),A3(6×4),A4(4×2),A5(2×7)矩阵顺序不能换,仍然只能按 A1A2A3A4A5 来乘。但括号可以换,而这正好决定了先算哪一部分。
例如:
| 括号方式 | 总次数 |
|---|
| ((((A1A2)A3)A4)A5) | 214 |
| ((A1(A2(A3A4)))A5) | 124 |
可以看到,两种写法的数学结果一样,但计算量不同。
所以,矩阵连乘问题真正关心的不是乘积本身,而是:
先算哪一部分,才能让总计算量最小。
3. 算法策略#
矩阵连乘问题使用:
动态规划原因是它满足动态规划的两个特点:
- 最优子结构:整体最优解可以由子问题的最优解组成。
- 重叠子问题:不同括号方式中会反复用到相同的子问题。
例如要求 A1A2A3 的最优计算方式,可以拆成:
A1∣A2A3或:
A1A2∣A3每一种断开方式都会产生左右两个子问题。动态规划的思路就是先求小区间的最优值,再逐步推出大区间的最优值。
4. 状态定义#
设矩阵 Ai 的规模为:
Ai=pi−1×pi其中,p 是维度数组。
例如:
p=[2, 3, 6, 4, 2, 7]则:
A1=p0×p1=2×3A2=p1×p2=3×6A3=p2×p3=6×4A4=p3×p4=4×2A5=p4×p5=2×7所以,矩阵 Ai 的行数是 pi−1,列数是 pi。
这里容易混淆的一点是:
- A1,A2,A3 是矩阵编号,通常从 1 开始。
- p0,p1,p2,p3 是维度数组下标,通常从 0 开始。
所以才会有:
Ai=pi−1×pi
动态规划中主要使用两张表:m 表和 s 表。
m[i][j]=计算 AiAi+1⋯Aj 的最少乘法次数s[i][j]=取得最优值时的断开位置 k
| 表 | 含义 | 作用 |
|---|
| m[i][j] | 从 Ai 乘到 Aj 的最少乘法次数 | 记录最优值 |
| s[i][j] | 从 Ai 乘到 Aj 时的最优断点 | 还原最优括号 |
例如:
m[1][5]表示计算 A1A2A3A4A5 的最少乘法次数。
如果:
s[1][5]=4表示最优断点在 A4 后面:
A1A2A3A4∣A5初始条件为:
m[i][i]=0因为单个矩阵不需要相乘。
5. 状态转移方程#
要求 m[i][j],也就是要求 Ai⋯Aj 的最少乘法次数。
可以在某个位置 k 处断开:
Ai⋯Ak∣Ak+1⋯Aj其中:
i≤k<j总代价由三部分组成:
总代价=左半部分代价+右半部分代价+最后合并代价所以状态转移方程为:
m[i][j]=i≤k<jmin(m[i][k]+m[k+1][j]+pi−1⋅pk⋅pj)其中:
- m[i][k] 表示左半部分 Ai⋯Ak 的最少乘法次数。
- m[k+1][j] 表示右半部分 Ak+1⋯Aj 的最少乘法次数。
- pi−1⋅pk⋅pj 表示左右两部分最后合并时的乘法次数。
- k 表示当前尝试的断点。
为什么最后合并代价是:
pi−1⋅pk⋅pj因为一串矩阵相乘后,结果矩阵的规模等于:
第一个矩阵的行数×最后一个矩阵的列数所以:
Ai⋯Ak 的结果规模为 pi−1×pkAk+1⋯Aj 的结果规模为 pk×pj最后合并时,相当于计算:
(pi−1×pk)×(pk×pj)因此乘法次数为:
pi−1⋅pk⋅pj用一句话理解这个公式:
枚举所有断点 k,把矩阵链分成左右两部分,先算左右两边的最优代价,再加上最后合并的代价,最后取最小值。
6. 算法步骤#
- 初始化 m[i][i]=0。
- 按矩阵链长度 l=2,3,…,n 从短到长填表。
- 对每个区间 [i, j],枚举断点 k。
- 使用状态转移方程计算当前代价。
- 如果当前代价更小,就更新 m[i][j],并把断点 k 记录到 s[i][j]。
- 最后,m[1][n] 就是最少乘法次数。
- 根据 s 表可以还原最优加括号方式。
7. 样例计算#
设维度数组为:
p=[2, 3, 6, 4, 2, 7]对应矩阵为:
A1(2×3),A2(3×6),A3(6×4),A4(4×2),A5(2×7)
7.1 长度为 2#
先计算两个矩阵相乘的情况。
m[1][2]=2×3×6=36m[2][3]=3×6×4=72m[3][4]=6×4×2=48m[4][5]=4×2×7=56
7.2 长度为 3#
计算 m[1][3]:
断点 k=1:
m[1][1]+m[2][3]+p0p1p3=0+72+2×3×4=96断点 k=2:
m[1][2]+m[3][3]+p0p2p3=36+0+2×6×4=84所以:
m[1][3]=84,s[1][3]=2
计算 m[2][4]:
断点 k=2:
0+48+3×6×2=84断点 k=3:
72+0+3×4×2=96所以:
m[2][4]=84,s[2][4]=2
计算 m[3][5]:
断点 k=3:
0+56+6×4×7=224断点 k=4:
48+0+6×2×7=132所以:
m[3][5]=132,s[3][5]=4
7.3 长度为 4#
计算 m[1][4]:
断点 k=1:
0+84+2×3×2=96断点 k=2:
36+48+2×6×2=108断点 k=3:
84+0+2×4×2=100所以:
m[1][4]=96,s[1][4]=1
计算 m[2][5]:
断点 k=2:
0+132+3×6×7=258断点 k=3:
72+56+3×4×7=212断点 k=4:
84+0+3×2×7=126所以:
m[2][5]=126,s[2][5]=4
7.4 长度为 5#
计算 m[1][5]:
断点 k=1:
0+126+2×3×7=168断点 k=2:
36+132+2×6×7=252断点 k=3:
84+56+2×4×7=196断点 k=4:
96+0+2×2×7=124所以:
m[1][5]=124,s[1][5]=4
8. 最优值表与决策表#
8.1 最优值表 m[i][j]#
| m[i][j] | j=1 | j=2 | j=3 | j=4 | j=5 |
|---|
| i=1 | 0 | 36 | 84 | 96 | 124 |
| i=2 | — | 0 | 72 | 84 | 126 |
| i=3 | — | — | 0 | 48 | 132 |
| i=4 | — | — | — | 0 | 56 |
| i=5 | — | — | — | — | 0 |
其中:
m[1][5]=124表示计算 A1A2A3A4A5 的最少乘法次数为 124。
8.2 最优决策表 s[i][j]#
| s[i][j] | j=1 | j=2 | j=3 | j=4 | j=5 |
|---|
| i=1 | — | 1 | 2 | 1 | 4 |
| i=2 | — | — | 2 | 2 | 4 |
| i=3 | — | — | — | 3 | 4 |
| i=4 | — | — | — | — | 4 |
| i=5 | — | — | — | — | — |
其中:
s[1][5]=4表示最优断点在 A4 后面:
A1A2A3A4∣A5
9. 如何根据 s 表画括号?#
s[i][j]=k 的意思是:
从 Ai 到 Aj 这一段,最优断点在 Ak 后面。
也就是:
Ai⋯Ak∣Ak+1⋯Aj画括号时要注意:
- 竖线 ∣ 表示“从哪里断开”。
- 括号表示“这一段要先作为一个整体计算”。
所以如果:
s[3][4]=3表示:
A3∣A4写成括号就是:
(A3A4)不是把竖线写进最终答案,而是把断开的左右两部分合成一个整体。
9.1 根据 s 表拆分#
从整体开始看:
s[1][5]=4所以:
A1A2A3A4∣A5继续看左半部分:
s[1][4]=1所以:
A1∣A2A3A4继续看右侧的 A2A3A4:
s[2][4]=2所以:
A2∣A3A4继续看右侧的 A3A4:
s[3][4]=3所以:
A3∣A4拆分过程可以写成:
9.2 从内向外加括号#
根据上面的拆分,最里面是:
A3∣A4所以先写:
(A3A4)然后把 A2 加进来:
A2(A3A4)加成一个整体:
(A2(A3A4))再把 A1 加进来:
A1(A2(A3A4))加成一个整体:
(A1(A2(A3A4)))最后把 A5 加进来:
((A1(A2(A3A4)))A5)所以,从内向外写括号的过程是:
A3A4→(A3A4)A2(A3A4)→(A2(A3A4))A1(A2(A3A4))→(A1(A2(A3A4)))(A1(A2(A3A4)))A5→((A1(A2(A3A4)))A5)最终最优括号为:
((A1(A2(A3A4)))A5)最少乘法次数为:
124
一句话总结:
断点告诉你从哪里切,括号告诉你哪一段先算。
10. Java 实现#
public class MatrixChainMultiplication {
static int[][] m; // m[i][j] 表示 Ai 到 Aj 的最少乘法次数
static int[][] s; // s[i][j] 表示最优断点位置
public static void matrixChainOrder(int[] p) {
int n = p.length - 1; // 矩阵个数
m = new int[n + 1][n + 1];
s = new int[n + 1][n + 1];
for (int i = 1; i <= n; i++) {
for (int len = 2; len <= n; len++) {
for (int i = 1; i <= n - len + 1; i++) {
m[i][j] = Integer.MAX_VALUE;
for (int k = i; k < j; k++) {
+ p[i - 1] * p[k] * p[j];
public static void printOptimalParens(int i, int j) {
System.out.print("A" + i);
printOptimalParens(i, s[i][j]);
printOptimalParens(s[i][j] + 1, j);
public static void main(String[] args) {
// p = [2, 3, 6, 4, 2, 7]
int[] p = {2, 3, 6, 4, 2, 7};
System.out.println("最少乘法次数:" + m[1][n]);
System.out.print("最优加括号方式:");
printOptimalParens(1, n);
运行结果:
最优加括号方式:((A1(A2(A3A4)))A5)
11. 总结#
矩阵连乘问题的关键不是计算矩阵乘积,而是寻找最优计算顺序。
核心内容可以概括为:
- 使用动态规划解决。
- m[i][j] 表示 Ai 到 Aj 的最少乘法次数。
- s[i][j] 表示取得最优值时的断开位置。
- k 表示当前尝试的断点。
- Ai=pi−1×pi。
- 一串矩阵相乘后的规模等于第一个矩阵的行数乘最后一个矩阵的列数。
- 状态转移方程为:
m[i][j]=i≤k<jmin(m[i][k]+m[k+1][j]+pi−1⋅pk⋅pj)通俗理解就是:
把矩阵链从不同位置切开,分别计算左右两边的最优代价,再加上最后合并的代价,最后选择代价最小的切法。