mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4mobile wallpaper 5mobile wallpaper 6
1764 字
5 分钟
矩阵连乘问题(动态规划)
2026-06-05 12:45:48

1. 问题描述#

给定矩阵序列 A1,A2,,AnA_1, A_2, \ldots, A_n,矩阵相乘顺序不能改变,但可以改变加括号的方式。

目标:找到一种加括号方式,使矩阵连乘的总计算次数最少。

需要注意的是,矩阵连乘问题不是要求真正算出矩阵乘积,而是要求找到最优计算顺序。


2. 括号的位置会影响计算次数#

矩阵相乘有一个很容易被忽略的点:先算谁,会改变中间结果的规模

对于两个矩阵 A(p×q)A(p \times q)B(q×r)B(q \times r),它们相乘需要的计算次数是:

p×q×rp \times q \times r

而乘完之后,得到的新矩阵规模是:

p×rp \times r

中间的 qq 只负责“对齐”两个矩阵,真正留下来的只有外侧的 pprr

(p×q)×(q×r)=p×r(p \times q) \times (q \times r) = p \times r

这就带来一个问题:如果有多个矩阵连乘,某一步产生的中间矩阵太大,后面的计算量也会被一起放大。

来看一个例子,维度数组为:

p=[2, 3, 6, 4, 2, 7]p = [2,\ 3,\ 6,\ 4,\ 2,\ 7]

对应 5 个矩阵:

A1(2×3),A2(3×6),A3(6×4),A4(4×2),A5(2×7)A_1(2 \times 3),\quad A_2(3 \times 6),\quad A_3(6 \times 4),\quad A_4(4 \times 2),\quad A_5(2 \times 7)

矩阵顺序不能换,仍然只能按 A1A2A3A4A5A_1A_2A_3A_4A_5 来乘。但括号可以换,而这正好决定了先算哪一部分。

例如:

括号方式总次数
((((A1A2)A3)A4)A5)((((A_1A_2)A_3)A_4)A_5)214
((A1(A2(A3A4)))A5)((A_1(A_2(A_3A_4)))A_5)124

可以看到,两种写法的数学结果一样,但计算量不同。

所以,矩阵连乘问题真正关心的不是乘积本身,而是:

先算哪一部分,才能让总计算量最小。


3. 算法策略#

矩阵连乘问题使用:

动态规划\boxed{\text{动态规划}}

原因是它满足动态规划的两个特点:

  • 最优子结构:整体最优解可以由子问题的最优解组成。
  • 重叠子问题:不同括号方式中会反复用到相同的子问题。

例如要求 A1A2A3A_1A_2A_3 的最优计算方式,可以拆成:

A1A2A3A_1 \mid A_2A_3

或:

A1A2A3A_1A_2 \mid A_3

每一种断开方式都会产生左右两个子问题。动态规划的思路就是先求小区间的最优值,再逐步推出大区间的最优值。


4. 状态定义#

设矩阵 AiA_i 的规模为:

Ai=pi1×piA_i = p_{i-1} \times p_i

其中,pp 是维度数组。

例如:

p=[2, 3, 6, 4, 2, 7]p = [2,\ 3,\ 6,\ 4,\ 2,\ 7]

则:

A1=p0×p1=2×3A_1 = p_0 \times p_1 = 2 \times 3A2=p1×p2=3×6A_2 = p_1 \times p_2 = 3 \times 6A3=p2×p3=6×4A_3 = p_2 \times p_3 = 6 \times 4A4=p3×p4=4×2A_4 = p_3 \times p_4 = 4 \times 2A5=p4×p5=2×7A_5 = p_4 \times p_5 = 2 \times 7

所以,矩阵 AiA_i 的行数是 pi1p_{i-1},列数是 pip_i

这里容易混淆的一点是:

  • A1,A2,A3A_1, A_2, A_3 是矩阵编号,通常从 11 开始。
  • p0,p1,p2,p3p_0, p_1, p_2, p_3 是维度数组下标,通常从 00 开始。

所以才会有:

Ai=pi1×piA_i = p_{i-1} \times p_i

动态规划中主要使用两张表:mm 表和 ss 表。

m[i][j]=计算 AiAi+1Aj 的最少乘法次数m[i][j] = \text{计算 } A_iA_{i+1}\cdots A_j \text{ 的最少乘法次数}s[i][j]=取得最优值时的断开位置 ks[i][j] = \text{取得最优值时的断开位置 } k
含义作用
m[i][j]m[i][j]AiA_i 乘到 AjA_j 的最少乘法次数记录最优值
s[i][j]s[i][j]AiA_i 乘到 AjA_j 时的最优断点还原最优括号

例如:

m[1][5]m[1][5]

表示计算 A1A2A3A4A5A_1A_2A_3A_4A_5 的最少乘法次数。

如果:

s[1][5]=4s[1][5] = 4

表示最优断点在 A4A_4 后面:

A1A2A3A4A5A_1A_2A_3A_4 \mid A_5

初始条件为:

m[i][i]=0m[i][i] = 0

因为单个矩阵不需要相乘。


5. 状态转移方程#

要求 m[i][j]m[i][j],也就是要求 AiAjA_i \cdots A_j 的最少乘法次数。

可以在某个位置 kk 处断开:

AiAkAk+1AjA_i \cdots A_k \mid A_{k+1} \cdots A_j

其中:

ik<ji \le k < j

总代价由三部分组成:

总代价=左半部分代价+右半部分代价+最后合并代价\text{总代价} = \text{左半部分代价} + \text{右半部分代价} + \text{最后合并代价}

所以状态转移方程为:

m[i][j]=minik<j(m[i][k]+m[k+1][j]+pi1pkpj)m[i][j] = \min_{i \le k < j} ( m[i][k] + m[k+1][j] + p_{i-1} \cdot p_k \cdot p_j )

其中:

  • m[i][k]m[i][k] 表示左半部分 AiAkA_i \cdots A_k 的最少乘法次数。
  • m[k+1][j]m[k+1][j] 表示右半部分 Ak+1AjA_{k+1} \cdots A_j 的最少乘法次数。
  • pi1pkpjp_{i-1} \cdot p_k \cdot p_j 表示左右两部分最后合并时的乘法次数。
  • kk 表示当前尝试的断点。

为什么最后合并代价是:

pi1pkpjp_{i-1} \cdot p_k \cdot p_j

因为一串矩阵相乘后,结果矩阵的规模等于:

第一个矩阵的行数×最后一个矩阵的列数\text{第一个矩阵的行数} \times \text{最后一个矩阵的列数}

所以:

AiAk 的结果规模为 pi1×pkA_i \cdots A_k \text{ 的结果规模为 } p_{i-1} \times p_kAk+1Aj 的结果规模为 pk×pjA_{k+1} \cdots A_j \text{ 的结果规模为 } p_k \times p_j

最后合并时,相当于计算:

(pi1×pk)×(pk×pj)(p_{i-1} \times p_k) \times (p_k \times p_j)

因此乘法次数为:

pi1pkpjp_{i-1} \cdot p_k \cdot p_j

用一句话理解这个公式:

枚举所有断点 kk,把矩阵链分成左右两部分,先算左右两边的最优代价,再加上最后合并的代价,最后取最小值。


6. 算法步骤#

  1. 初始化 m[i][i]=0m[i][i] = 0
  2. 按矩阵链长度 l=2,3,,nl = 2, 3, \ldots, n 从短到长填表。
  3. 对每个区间 [i, j][i,\ j],枚举断点 kk
  4. 使用状态转移方程计算当前代价。
  5. 如果当前代价更小,就更新 m[i][j]m[i][j],并把断点 kk 记录到 s[i][j]s[i][j]
  6. 最后,m[1][n]m[1][n] 就是最少乘法次数。
  7. 根据 ss 表可以还原最优加括号方式。

7. 样例计算#

设维度数组为:

p=[2, 3, 6, 4, 2, 7]p = [2,\ 3,\ 6,\ 4,\ 2,\ 7]

对应矩阵为:

A1(2×3),A2(3×6),A3(6×4),A4(4×2),A5(2×7)A_1(2 \times 3),\quad A_2(3 \times 6),\quad A_3(6 \times 4),\quad A_4(4 \times 2),\quad A_5(2 \times 7)

7.1 长度为 2#

先计算两个矩阵相乘的情况。

m[1][2]=2×3×6=36m[1][2] = 2 \times 3 \times 6 = 36m[2][3]=3×6×4=72m[2][3] = 3 \times 6 \times 4 = 72m[3][4]=6×4×2=48m[3][4] = 6 \times 4 \times 2 = 48m[4][5]=4×2×7=56m[4][5] = 4 \times 2 \times 7 = 56

7.2 长度为 3#

计算 m[1][3]m[1][3]

断点 k=1k=1

m[1][1]+m[2][3]+p0p1p3=0+72+2×3×4=96m[1][1] + m[2][3] + p_0p_1p_3 = 0 + 72 + 2 \times 3 \times 4 = 96

断点 k=2k=2

m[1][2]+m[3][3]+p0p2p3=36+0+2×6×4=84m[1][2] + m[3][3] + p_0p_2p_3 = 36 + 0 + 2 \times 6 \times 4 = 84

所以:

m[1][3]=84,s[1][3]=2m[1][3] = 84,\quad s[1][3] = 2

计算 m[2][4]m[2][4]

断点 k=2k=2

0+48+3×6×2=840 + 48 + 3 \times 6 \times 2 = 84

断点 k=3k=3

72+0+3×4×2=9672 + 0 + 3 \times 4 \times 2 = 96

所以:

m[2][4]=84,s[2][4]=2m[2][4] = 84,\quad s[2][4] = 2

计算 m[3][5]m[3][5]

断点 k=3k=3

0+56+6×4×7=2240 + 56 + 6 \times 4 \times 7 = 224

断点 k=4k=4

48+0+6×2×7=13248 + 0 + 6 \times 2 \times 7 = 132

所以:

m[3][5]=132,s[3][5]=4m[3][5] = 132,\quad s[3][5] = 4

7.3 长度为 4#

计算 m[1][4]m[1][4]

断点 k=1k=1

0+84+2×3×2=960 + 84 + 2 \times 3 \times 2 = 96

断点 k=2k=2

36+48+2×6×2=10836 + 48 + 2 \times 6 \times 2 = 108

断点 k=3k=3

84+0+2×4×2=10084 + 0 + 2 \times 4 \times 2 = 100

所以:

m[1][4]=96,s[1][4]=1m[1][4] = 96,\quad s[1][4] = 1

计算 m[2][5]m[2][5]

断点 k=2k=2

0+132+3×6×7=2580 + 132 + 3 \times 6 \times 7 = 258

断点 k=3k=3

72+56+3×4×7=21272 + 56 + 3 \times 4 \times 7 = 212

断点 k=4k=4

84+0+3×2×7=12684 + 0 + 3 \times 2 \times 7 = 126

所以:

m[2][5]=126,s[2][5]=4m[2][5] = 126,\quad s[2][5] = 4

7.4 长度为 5#

计算 m[1][5]m[1][5]

断点 k=1k=1

0+126+2×3×7=1680 + 126 + 2 \times 3 \times 7 = 168

断点 k=2k=2

36+132+2×6×7=25236 + 132 + 2 \times 6 \times 7 = 252

断点 k=3k=3

84+56+2×4×7=19684 + 56 + 2 \times 4 \times 7 = 196

断点 k=4k=4

96+0+2×2×7=12496 + 0 + 2 \times 2 \times 7 = 124

所以:

m[1][5]=124,s[1][5]=4m[1][5] = 124,\quad s[1][5] = 4

8. 最优值表与决策表#

8.1 最优值表 m[i][j]m[i][j]#

m[i][j]m[i][j]j=1j=1j=2j=2j=3j=3j=4j=4j=5j=5
i=1i=10368496124
i=2i=207284126
i=3i=3048132
i=4i=4056
i=5i=50

其中:

m[1][5]=124m[1][5] = 124

表示计算 A1A2A3A4A5A_1A_2A_3A_4A_5 的最少乘法次数为 124124


8.2 最优决策表 s[i][j]s[i][j]#

s[i][j]s[i][j]j=1j=1j=2j=2j=3j=3j=4j=4j=5j=5
i=1i=11214
i=2i=2224
i=3i=334
i=4i=44
i=5i=5

其中:

s[1][5]=4s[1][5] = 4

表示最优断点在 A4A_4 后面:

A1A2A3A4A5A_1A_2A_3A_4 \mid A_5

9. 如何根据 ss 表画括号?#

s[i][j]=ks[i][j] = k 的意思是:

AiA_iAjA_j 这一段,最优断点在 AkA_k 后面。

也就是:

AiAkAk+1AjA_i \cdots A_k \mid A_{k+1} \cdots A_j

画括号时要注意:

  • 竖线 \mid 表示“从哪里断开”。
  • 括号表示“这一段要先作为一个整体计算”。

所以如果:

s[3][4]=3s[3][4] = 3

表示:

A3A4A_3 \mid A_4

写成括号就是:

(A3A4)(A_3A_4)

不是把竖线写进最终答案,而是把断开的左右两部分合成一个整体。


9.1 根据 ss 表拆分#

从整体开始看:

s[1][5]=4s[1][5] = 4

所以:

A1A2A3A4A5A_1A_2A_3A_4 \mid A_5

继续看左半部分:

s[1][4]=1s[1][4] = 1

所以:

A1A2A3A4A_1 \mid A_2A_3A_4

继续看右侧的 A2A3A4A_2A_3A_4

s[2][4]=2s[2][4] = 2

所以:

A2A3A4A_2 \mid A_3A_4

继续看右侧的 A3A4A_3A_4

s[3][4]=3s[3][4] = 3

所以:

A3A4A_3 \mid A_4

拆分过程可以写成:

A1 A2 A3 A4 A5
|
k=4
A1 A2 A3 A4 | A5
A1 A2 A3 A4
|
k=1
A1 | A2 A3 A4
A2 A3 A4
|
k=2
A2 | A3 A4
A3 A4
|
k=3
A3 | A4

9.2 从内向外加括号#

根据上面的拆分,最里面是:

A3A4A_3 \mid A_4

所以先写:

(A3A4)(A_3A_4)

然后把 A2A_2 加进来:

A2(A3A4)A_2(A_3A_4)

加成一个整体:

(A2(A3A4))(A_2(A_3A_4))

再把 A1A_1 加进来:

A1(A2(A3A4))A_1(A_2(A_3A_4))

加成一个整体:

(A1(A2(A3A4)))(A_1(A_2(A_3A_4)))

最后把 A5A_5 加进来:

((A1(A2(A3A4)))A5)((A_1(A_2(A_3A_4)))A_5)

所以,从内向外写括号的过程是:

A3A4(A3A4)A_3A_4 \rightarrow (A_3A_4)A2(A3A4)(A2(A3A4))A_2(A_3A_4) \rightarrow (A_2(A_3A_4))A1(A2(A3A4))(A1(A2(A3A4)))A_1(A_2(A_3A_4)) \rightarrow (A_1(A_2(A_3A_4)))(A1(A2(A3A4)))A5((A1(A2(A3A4)))A5)(A_1(A_2(A_3A_4)))A_5 \rightarrow ((A_1(A_2(A_3A_4)))A_5)

最终最优括号为:

((A1(A2(A3A4)))A5)\boxed{((A_1(A_2(A_3A_4)))A_5)}

最少乘法次数为:

124\boxed{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++) {
m[i][i] = 0;
}
// len 表示当前矩阵链长度
for (int len = 2; len <= n; len++) {
for (int i = 1; i <= n - len + 1; i++) {
int j = i + len - 1;
m[i][j] = Integer.MAX_VALUE;
// 枚举断点 k
for (int k = i; k < j; k++) {
int cost = m[i][k]
+ m[k + 1][j]
+ p[i - 1] * p[k] * p[j];
if (cost < m[i][j]) {
m[i][j] = cost;
s[i][j] = k;
}
}
}
}
}
public static void printOptimalParens(int i, int j) {
if (i == j) {
System.out.print("A" + i);
} else {
System.out.print("(");
printOptimalParens(i, s[i][j]);
printOptimalParens(s[i][j] + 1, j);
System.out.print(")");
}
}
public static void main(String[] args) {
// p = [2, 3, 6, 4, 2, 7]
// A1: 2×3
// A2: 3×6
// A3: 6×4
// A4: 4×2
// A5: 2×7
int[] p = {2, 3, 6, 4, 2, 7};
int n = p.length - 1;
matrixChainOrder(p);
System.out.println("最少乘法次数:" + m[1][n]);
System.out.print("最优加括号方式:");
printOptimalParens(1, n);
}
}

运行结果:

最少乘法次数:124
最优加括号方式:((A1(A2(A3A4)))A5)

11. 总结#

矩阵连乘问题的关键不是计算矩阵乘积,而是寻找最优计算顺序。

核心内容可以概括为:

  • 使用动态规划解决。
  • m[i][j]m[i][j] 表示 AiA_iAjA_j 的最少乘法次数。
  • s[i][j]s[i][j] 表示取得最优值时的断开位置。
  • kk 表示当前尝试的断点。
  • Ai=pi1×piA_i = p_{i-1} \times p_i
  • 一串矩阵相乘后的规模等于第一个矩阵的行数乘最后一个矩阵的列数。
  • 状态转移方程为:
m[i][j]=minik<j(m[i][k]+m[k+1][j]+pi1pkpj)m[i][j] = \min_{i \le k < j} ( m[i][k] + m[k+1][j] + p_{i-1} \cdot p_k \cdot p_j )

通俗理解就是:

把矩阵链从不同位置切开,分别计算左右两边的最优代价,再加上最后合并的代价,最后选择代价最小的切法。

分享

如果这篇文章对你有帮助,欢迎分享给更多人!

矩阵连乘问题(动态规划)
https://dawn114514.site/posts/algorithm/matrixchain/
作者
黎明
发布于
2026-06-05 12:45:48
许可协议
MIT

部分信息可能已经过时

目录