专栏文章

U519157 矩阵连乘题解

算法·理论参与者 1已保存评论 0

文章操作

快速查看文章及其快照的属性,并进行相关操作。

当前评论
0 条
当前快照
1 份
快照标识符
@miqnrni0
此快照首次捕获于
2025/12/04 07:48
3 个月前
此快照最后确认于
2025/12/04 07:48
3 个月前
查看原文

U519157 矩阵连乘题解

请注意:本题改编自P1753 矩阵链排序问题

这道题用到了动态规划,我想有必要在真正开始讲题之前讲讲动态规划。
所谓动态规划,其实就是分治的特例。分治的方法分三步:
第一步:把大象塞进冰箱里(doge
  1. 把原始问题分解乘除规模变小外与原始问题相同的子问题
  2. 若干子问题较小而容易被解决则直接解决,否则再继续分解为更小的子问题,直到容易解决。
  3. 将已求解的各个子问题的解,逐步合并为原问题的解。
有的问题分解后不需要合并子问题的解,此时就不需要再做第3步了[例如快速排序,只要把子问题(或称为子序列)排好序原问题就结束了]。多数问题需要子问题的解,按照题意使用恰当的方法合并成为整个问题的解。需要具体问题具体分析。

说完了分治,我们再回过头来看动态规划
什么时候是用动态规划呢?在分治分割出的子问题重复性高时使用动态规划 (这里隐含一个条件,那就是子问题必须以整个子问题为单位重复出现,不能重复子问题的局部)
于是我们的解决方法是以空间换时间。即保存全部子问题解(因为你不知道哪个子问题会重复,所以你只能全记下来),当遇到相同时调用。
需保证查找子问题界的时间 << 计算子问题解的时间 要不然你找个集贸啊,直接算不就得了 所以我们只能选择以下标的查找,以比较为基础的查找最坏情况下最好也才O(nlogn)O(n\log n)
实现方法:一般使用二维数组(是为了建立子问题与下标的映射关系,且一一对应)保存已解决出的子问题的解

接下来,我们来看题。
预备知识:矩阵乘法
矩阵乘法常常指的是一般矩阵乘法。设矩阵A=(aij)m×nA = (a_{ij})_{m \times n}B=(bij)m×sB = (b_{ij})_{m \times s},令C=(cij)m×sC=(c_{ij})_{m \times s}。其中cij=k=1naikbkj(1im,1js)c_{ij} = \displaystyle\sum_{k=1}^na_{ik}b_{kj}(1 \leq i \leq m,1\leq j\leq s),那么矩阵CC称为矩阵AABB的乘积,记为C=ABC=ABC=ABC=A \cdot B。为方便,称被乘数AA为左矩阵,乘数BB为右矩阵。
注意事项:
  • 只有左矩阵的列数与右矩阵的行数相同的两个矩阵才能相乘。
  • 乘积矩阵第i行第j列处的元素等于左矩阵的第i行与右矩阵的第j列对应元素乘积之和,即(AB)ij=k=1naikbkj\big( AB\big)_{ij}=\displaystyle\sum_{k=1}^na_{ik}b_{kj}
  • 乘积矩阵的行数等于左矩阵的行数,列数等于右矩阵的列数。
计算示例: 设A=[123045]A = \begin{bmatrix}1 & -2 \\ 3 & 0 \\ -4 & 5\end{bmatrix},B=[78910]B = \begin{bmatrix}-7 & 8 \\ 9 & 10\end{bmatrix},下面计算ABAB:
AB=[123045][78910]=[1×(7)+(2)×91×8+(2)×103×(7)+0×93×8+0×10(4)×(7)+5×9(4)×8+5×10]=[251221257318]AB=\begin{bmatrix}1 & -2 \\ 3 & 0 \\ -4 & 5\end{bmatrix}\cdotp\begin{bmatrix}-7 & 8 \\ 9 & 10\end{bmatrix}\\ =\begin{bmatrix}1\times(-7)+(-2)\times9 & 1\times 8+(-2)\times10\\3\times(-7)+0\times9 & 3\times8+0\times10\\(-4)\times(-7)+5\times9 & (-4)\times8+5\times10\end{bmatrix} \\=\begin{bmatrix}-25 & -12 \\ -21 & 25 \\ 73 & 18\end{bmatrix}
也就是说,矩阵乘法不具有交换律,但具有结合律(这个也叫矩阵乘法的完全加括号)。根据矩阵乘法规则可以得出mmnn列的AA矩阵乘nnkk列的BB矩阵的乘法次数为nmknmk,证明略。
而不同的加括号方式(或者叫做结合方式)所求的乘法次数不同,而这道题让我们求的就是由nn个矩阵连乘组成的矩阵链的最小乘法次数。
理清题意,我们就开始考虑用哪种算法实现。
  1. 穷举 在穷举算法的前提下,这道题就是让我们求 “有nn个数相乘,在括号里只能有两个数的情况下根据乘法分配律加括号,问有多少种加括号的方法” 。答案是第(n1)(n-1)个卡特兰数(证明在附录)。我们观察数据范围:1n5001 \leq n \leq 500h(499)=135,279,399,872,590,875,633,440,787,600,588,225,974,050,054,277,551,695,198,895,332,886,198,913,266,027,124,073,379,621,583,835,020,102,784,087,129,640,413,465,866,971,846,872,212,170,945,893,002,852,611,849,561,394,136,268,144,010,688,770,002,041,910,854,526,708,996,076,636,385,187,472,995,488,366,510,450,708,008,505,615,328,704,888,346,274,576,144,575,877,119,333,388,036,489,421,321,231,840h(499 )=135,279,399,872,590,875,633,440,787,600,588,225,974,050,054,277,551,695,198,895,332,886,198,913,266,027,124,073,379,621,583,835,020,102,784,087,129,640,413,465,866,971,846,872,212,170,945,893,002,852,611,849,561,394,136,268,144,010,688,770,002,041,910,854,526,708,996,076,636,385,187,472,995,488,366,510,450,708,008,505,615,328,704,888,346,274,576,144,575,877,119,333,388,036,489,421,321,231,840这么庞大的数,绝对超时,所以不考虑
  2. 动态规划
在专业课本上对于动态规划的步骤解释是这样的:
  1. 找出最优解的性质(分析最优解的结构),并刻画其结构特征
  2. 递归地定义最优值
  3. 以自底向上的方式计算出最优值
  4. 根据计算最优值时得到的信息,构造最优解(可选)
是不是感觉一头雾水?接下来我用这道题解释

找出最优解的性质(分析最优解的结构),并刻画其结构特征其实就是判断是否能分割和合并,也有人叫它最有子问题性质。说人话就是“你这个问题能用分治吗?能:构造出分割和合并的方案;否:break;”
对于本题,很显然,我们可以利用乘法分配律从中间切开,例如:
(Ai×Ai+1××Ax)×(Ax+1×Ax+2××Aj)(A_i \times A_{i + 1} \times \cdots \times A_x)\times(A_{x + 1} \times A_{x + 2} \times \cdots \times A_j)
为了方便表示,我们令AiAi+1Aj1AjA_iA_{i + 1}\cdots A_{j-1}A_j记为Ai,j,ijA_{i, j},i \leq j。按理来说,我们希望的是在最优的那个位置切分。但问题是我们不知道在哪里切是最优解,所以我们只能挨个试。(题外话,我们老师自己给这个方法起了个名字叫穷举式切割) 所以我们设:最优计算次序是在矩阵AkA_kAk+1A_{k+1}之间分割(ik<j)(i \leq k < j)。则对应的分割方案为
(Ai×Ai+1××Ak)×(Ak+1×Ak+2××Aj)(A_i \times A_{i + 1} \times \cdots \times A_k)\times(A_{k + 1} \times A_{k + 2} \times \cdots \times A_j)
其乘法计算量为:Ai,kA_{i,k}计算量 ++ Ak+1,jA_{k+1,j}计算量++ Ai,k×Ak+1,jA_{i,k} \times A_{k+1,j}计算量(Ai,k×Ak+1,jA_{i,k} \times A_{k+1,j}计算量为合并代价)
递归地定义最优值(建立递归关系),其对应的就是分治的分别求解。在这里,动态规划的优点就体现出来了。我们设:计算Ai,j,1ijnA_{i,j},1 \leq i \leq j \leq n,所需的最小乘法次数为Mi,jM_{i,j}
i=ji = jAi,j=AiA_{i,j} = A_i。此时因为只有一个数,不用进行乘法运算,所以它的乘法次数为00。即:Mi,j=Mi,i=0M_{i,j} = M_{i,i} = 0
i<ji < jMi,j=Ai,kM_{i,j} = A_{i,k}计算量 ++ Ak+1,jA_{k+1,j}计算量++ Ai,k×Ak+1,jA_{i,k} \times A_{k+1,j}计算量 = Mi,jM_{i,j} ++ Mk+1,jM_{k+1,j} ++ Mi,j×Mk+1,jM_{i,j} \times M_{k+1,j} 。而根据我们之前说的:
根据矩阵乘法规则可以得出mmnn列的AA矩阵乘nnkk列的BB矩阵的乘法次数为nmknmk,证明略。
则可以判断出,Mi,j×Mk+1,jM_{i,j} \times M_{k+1,j}的计算量为pi1pkpjp_{i-1}p_kp_j (因为我在输入pp序列的时候使用的是0开头的存储方式,所以AxA_x矩阵的行列为px1p_{x-1}pxp_x)
最终公式为Mi,jM_{i,j} ++ Mk+1,jM_{k+1,j} ++ pi1pkpj(ik<j)p_{i-1}p_kp_j (i \leq k < j)
则原问题的最优解就是从第一个矩阵乘到第nn个矩阵的最优解,即M[1,n]M[1,n]
请注意,动态规划最重要的一步来啦:列出递归方程
M[i,j]={0i=jminik<jM[i,k]+M[k+1,j]+pi1pkpji<j\large M[i,j] = \begin{cases} 0 & i=j \\ \min\limits_{i \leq k < j}{M[i,k]+M[k+1,j]+p_{i-1}p_kp_j} & i<j \end{cases}
以自底向上的方式计算出最优值 由从左上到右下,所以是非递归实现
根据计算最优值时得到的信息,构造最优解 我们这时的最优解就是乘法顺序方案,可以用一个s数组记录。然后写一个print函数,使用递归分别递归左右两侧。
核心代码:
CPP
int n, p[510], m[510][510], s[510][510];

void MatrixChain(int n) {
	//计算所有长度为 1 的子问题 A[i:i] 的最小计算次数 (递归出口)
	for(int i = 1; i <= n; i++) m[i][i] = 0;						 
	
	//计算长度为 2 ~ n 的子问题 
	for(int r = 2; r <= n; r++) {
		//r控制子问题长度,保证从 2 到 n 的顺序计算计算子问题 
		for(int i = 1; i <= n - r + 1; i++) {
			/* i 控制长度为 r 的子问题的起始矩阵下标 
			   n - r + 1是保证长度为 r 的矩阵链成立的最大下标 */ 
			int j = i + r - 1;		//长度为 r 的矩阵链的最后一个下标
			
			/*尝试不同切割, 找出最优切割 ( i <= k < j, 相当于尝试不同的 k ) 
			
                  /
                  |								0								i = j
           m[i] = | 
                  |  min(i <= k < j){ m[i][k] + m[k+1][j] + P[i-1]P[k]P[j] }	i < j
                  \
			*/
			
			m[i][j] = /*m[i][i] + */m[i + 1][j] + p[i - 1] * p[i] * p[j];	// k = i 的情况
			//实际上 m[i][i]可以省略, 因为值是 0 
			s[i][j] = i;
			//s数组是用来记录最优分割的位置的
			
			//其它 k 取值 ( i + 1 ~ j + 1 ) ——其他切割 
			for(int k = i + 1; k < j; k++) {
				int t = m[i][k] + m[k + 1][j] + p[i - 1] * p[k] * p[j];
				if(t < m[i][j]) {
					m[i][j] = t;
					s[i][j] = k;
				}
			}
		}
	}
}

void print(int i, int j) {
	if(i == j) printf("A%d", i);
	else {
		printf("(");
		print(i, s[i][j]);
		print(s[i][j] + 1, j);
		printf(")");
	}
}

根据这道题,我们来总结一下动态规划算法
动态规划基本要素:最优子结构性质(可分可和) & 重叠子问题性质(以整个子问题为单位重复出现)
最优子结构性质:问题的最优解包含着其子问题的最优解(专业需证明,走竞赛的就不用了)
重叠子问题性质:每次出现(产生)的子问题并不总是新问题,有些子问题被重复计算多次。

附录:卡特兰数推导
对于nn个数相乘,且每次括号里只能包含两个数,我们可以通过不同的方式加括号来改变计算的顺序。例如,对于三个数aabbcc,有两种括号化方式:(ab)c(ab)ca(bc)a(bc)。对于四个数aabbccdd,有五种括号化方式:((ab)c)d((ab)c)d(a(bc))d(a(bc))d(ab)(cd)(ab)(cd)a((bc)d)a((bc)d)a(b(cd))a(b(cd))。不难发现,随着n的增加,括号化的方式会迅速增多。这个问题实际上与组合数学中的卡特兰数(Catalan number)有关。对于nn个数相乘的括号化方式,其数量等于第(n1)(n-1)个卡特兰数。卡特兰数的通项公式为:h(n)=C2nnn+1h(n)=\displaystyle\frac{C_{2n}^{n}}{n + 1}。其中,C2nnC_{2n}^{n}是二项式系数,表示从2n2n个元素中选取nn个元素的组合数。但是,对于括号化乘法的问题,更直接的递推公式是:h(n)=k=1n1h(k)h(nk)h(n)=\displaystyle\sum_{k=1}^{n-1}h(k)h(n-k)其中,h(1)=1h(1) = 1。这个递推公式的意思是,对于nn个数的括号化,可以选择第一个操作是前kk个数和后(nk)(n-k)个数的乘积的乘积,其中kk11n1n-1。例如,对于n=3n=3h(3)=h(1)h(2)+h(2)h(1)=11+11=2h(3) = h(1)h(2) +h(2)h(1) = 11 + 11 = 2对于n=4n=4h(4)=h(1)h(3)+h(2)h(2)+h(3)h(1)=12+11+21=5h(4) = h(1)h(3) + h(2)h(2) + h(3)h(1) = 12 + 11 + 21 = 5这与我们之前列举的四种情况相符。因此,nn个数相乘,每次括号里只能包含两个数的加括号方法数是第(n1)(n-1)个卡特兰数。所以,答案是第(n1)(n-1)个卡特兰数。

评论

0 条评论,欢迎与作者交流。

正在加载评论...