专栏文章
U519157 矩阵连乘题解
算法·理论参与者 1已保存评论 0
文章操作
快速查看文章及其快照的属性,并进行相关操作。
- 当前评论
- 0 条
- 当前快照
- 1 份
- 快照标识符
- @miqnrni0
- 此快照首次捕获于
- 2025/12/04 07:48 3 个月前
- 此快照最后确认于
- 2025/12/04 07:48 3 个月前
U519157 矩阵连乘题解
请注意:本题改编自P1753 矩阵链排序问题
这道题用到了动态规划,我想有必要在真正开始讲题之前讲讲动态规划。
所谓动态规划,其实就是分治的特例。分治的方法分三步:
- 把原始问题分解乘除规模变小外与原始问题相同的子问题
- 若干子问题较小而容易被解决则直接解决,否则再继续分解为更小的子问题,直到容易解决。
- 将已求解的各个子问题的解,逐步合并为原问题的解。
有的问题分解后不需要合并子问题的解,此时就不需要再做第3步了[例如快速排序,只要把子问题(或称为子序列)排好序原问题就结束了]。多数问题需要子问题的解,按照题意使用恰当的方法合并成为整个问题的解。需要具体问题具体分析。
说完了分治,我们再回过头来看动态规划
什么时候是用动态规划呢?在分治分割出的子问题重复性高时使用动态规划
(这里隐含一个条件,那就是子问题必须以整个子问题为单位重复出现,不能重复子问题的局部)
于是我们的解决方法是以空间换时间。即保存全部子问题解(因为你不知道哪个子问题会重复,所以你只能全记下来),当遇到相同时调用。
需保证查找子问题界的时间 计算子问题解的时间
要不然你找个集贸啊,直接算不就得了
所以我们只能选择以下标的查找,以比较为基础的查找最坏情况下最好也才
实现方法:一般使用二维数组(是为了建立子问题与下标的映射关系,且一一对应)保存已解决出的子问题的解
接下来,我们来看题。
预备知识:矩阵乘法矩阵乘法常常指的是一般矩阵乘法。设矩阵和,令。其中,那么矩阵称为矩阵与的乘积,记为 或 。为方便,称被乘数为左矩阵,乘数为右矩阵。注意事项:
- 只有左矩阵的列数与右矩阵的行数相同的两个矩阵才能相乘。
- 乘积矩阵第i行第j列处的元素等于左矩阵的第i行与右矩阵的第j列对应元素乘积之和,即。
- 乘积矩阵的行数等于左矩阵的行数,列数等于右矩阵的列数。
计算示例: 设,,下面计算:
也就是说,矩阵乘法不具有交换律,但具有结合律(这个也叫矩阵乘法的完全加括号)。根据矩阵乘法规则可以得出行列的矩阵乘行列的矩阵的乘法次数为,证明略。
而不同的加括号方式(或者叫做结合方式)所求的乘法次数不同,而这道题让我们求的就是由个矩阵连乘组成的矩阵链的最小乘法次数。
理清题意,我们就开始考虑用哪种算法实现。
- 穷举 在穷举算法的前提下,这道题就是让我们求 “有个数相乘,在括号里只能有两个数的情况下根据乘法分配律加括号,问有多少种加括号的方法” 。答案是第个卡特兰数(证明在附录)。我们观察数据范围:,这么庞大的数,绝对超时,所以不考虑
- 动态规划
在专业课本上对于动态规划的步骤解释是这样的:
- 找出最优解的性质(分析最优解的结构),并刻画其结构特征
- 递归地定义最优值
- 以自底向上的方式计算出最优值
- 根据计算最优值时得到的信息,构造最优解(可选)
是不是感觉一头雾水?接下来我用这道题解释
找出最优解的性质(分析最优解的结构),并刻画其结构特征其实就是判断是否能分割和合并,也有人叫它最有子问题性质。说人话就是“你这个问题能用分治吗?能:构造出分割和合并的方案;否:break;”
对于本题,很显然,我们可以利用乘法分配律从中间切开,例如:
为了方便表示,我们令记为。按理来说,我们希望的是在最优的那个位置切分。但问题是我们不知道在哪里切是最优解,所以我们只能挨个试。(题外话,我们老师自己给这个方法起了个名字叫穷举式切割) 所以我们设:最优计算次序是在矩阵和之间分割。则对应的分割方案为
其乘法计算量为:计算量 计算量 计算量(计算量为合并代价)
递归地定义最优值(建立递归关系),其对应的就是分治的分别求解。在这里,动态规划的优点就体现出来了。我们设:计算,所需的最小乘法次数为
当,。此时因为只有一个数,不用进行乘法运算,所以它的乘法次数为。即:
当,计算量 计算量 计算量 = 。而根据我们之前说的:
根据矩阵乘法规则可以得出行列的矩阵乘行列的矩阵的乘法次数为,证明略。
则可以判断出,的计算量为 (因为我在输入序列的时候使用的是0开头的存储方式,所以矩阵的行列为和)
最终公式为
则原问题的最优解就是从第一个矩阵乘到第个矩阵的最优解,即
请注意,动态规划最重要的一步来啦:列出递归方程
以自底向上的方式计算出最优值 由从左上到右下,所以是非递归实现
根据计算最优值时得到的信息,构造最优解 我们这时的最优解就是乘法顺序方案,可以用一个s数组记录。然后写一个print函数,使用递归分别递归左右两侧。
核心代码:
CPPint 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(")");
}
}
根据这道题,我们来总结一下动态规划算法
动态规划基本要素:最优子结构性质(可分可和) & 重叠子问题性质(以整个子问题为单位重复出现)
最优子结构性质:问题的最优解包含着其子问题的最优解(专业需证明,走竞赛的就不用了)
重叠子问题性质:每次出现(产生)的子问题并不总是新问题,有些子问题被重复计算多次。
附录:卡特兰数推导
对于个数相乘,且每次括号里只能包含两个数,我们可以通过不同的方式加括号来改变计算的顺序。例如,对于三个数、、,有两种括号化方式: 和 。对于四个数、、、,有五种括号化方式:、、、、。不难发现,随着n的增加,括号化的方式会迅速增多。这个问题实际上与组合数学中的卡特兰数(Catalan number)有关。对于个数相乘的括号化方式,其数量等于第个卡特兰数。卡特兰数的通项公式为:。其中,是二项式系数,表示从个元素中选取个元素的组合数。但是,对于括号化乘法的问题,更直接的递推公式是:其中,。这个递推公式的意思是,对于个数的括号化,可以选择第一个操作是前个数和后个数的乘积的乘积,其中从到。例如,对于:对于:这与我们之前列举的四种情况相符。因此,个数相乘,每次括号里只能包含两个数的加括号方法数是第个卡特兰数。所以,答案是第个卡特兰数。
相关推荐
评论
共 0 条评论,欢迎与作者交流。
正在加载评论...