【问题描述】
给定 个矩阵 ,其中, 和 是可乘的。用加括号的方法表示矩阵连乘的次序,不同的计算次序计算量(乘法次数)是不同的,找出一种加括号的方法,使得矩阵连乘的计算量最小。
例如:
是 的矩阵; 是 的矩阵; 是 的矩阵。
那么有两种加括号的方法:
- ;
第 种加括号方法运算量:
第 种加括号方法运算量:
可以看出,不同的加括号办法,矩阵乘法的运算次数可能有巨大的差别!
【问题分析】
1.前置知识:
(1)什么是矩阵可乘?
如果两个矩阵,第 个矩阵的列等于第 个矩阵的行时,那么这两个矩阵是可乘的。
(2)矩阵相乘后的结果是什么?
两个矩阵相乘的结果矩阵,其行、列分别等于第 个矩阵的行、第 个矩阵的列。
多个矩阵相乘的结果矩阵,其行、列分别等于第 个矩阵的行、最后 个矩阵的列。而且无论矩阵的计算次序如何都不影响它们的结果矩阵。
(3)两个矩阵相乘需要多少次乘法?
两个矩阵 、 相乘执行乘法运算的次数为 。
因此,、 相乘执行乘法运算的次数为 。
(4)穷举
如果穷举所有的加括号方法,那么加括号的所有方案是一个卡特兰数序列,其算法时间复杂度为 ,是指数阶,因此穷举的办法是很糟的。
2.动态规划
(1)分析最优解的结构特征
假设我们已经知道了在第 个位置加括号会得到最优解,那么原问题就变成了两个子问题:,。
原问题的最优解是否包含子问题的最优解呢?
假设 的乘法次数是 , 的乘法次数是 , 的乘法次数是 , 和 的结果矩阵相乘的乘法次数是 ,那么 。
而无论两个子问题 、 的计算次序如何,都不影响它们结果矩阵,两个结果矩阵相乘的乘法次数 不变。
因此,我们只需要证明:如果 是最优的,则 和 一定是最优的(即原问题的最优解包含子问题的最优解)。
反证法:如果 不是最优的, 存在一个最优解 ,,那么,,所以 不是最优的,这与假设 是最优的矛盾,因此,如果 是最优的,则 一定是最优的。同理,可证 也是最优的。因此,如果 是最优的,则 和 一定是最优的。
因此,矩阵连乘问题具有最优子结构性质。
(2)建立最优值递归式
用二维数组 表示 矩阵连乘的最优值,那么两个子问题 、 对应的最优值分别是 、。剩下的只需要考查 和 的结果矩阵相乘的乘法次数了。
设矩阵 的行数为 ,列数为 ,,且矩阵是可乘的,即相邻矩阵前一个矩阵的列等于下一个矩阵的行 。 的结果是一个 矩阵, 的结果是一个 矩阵,,两个结果矩阵相乘的乘法次数是 。
用一维数组 来记录矩阵的行和列,第 个矩阵的行数存储在数组的第 位置,列数存储在数组的第 位置,那么 对应的数组元素相乘为 ,递归式为:
(3)自底向上计算并记录最优值
初始化:采用一维数组 来记录矩阵的行和列,,,其中
先求 个矩阵相乘的最优值,再求 个矩阵相乘的最优值,直到 个矩阵连乘的最优值。
- 按照递归关系式计算 个矩阵 、 相乘时的最优值,,并将其存入 ,同时将最优策略记入 ,。
- 按照递归关系式计算 个矩阵相乘 、、 相乘时的最优值,,并将其存入 ,同时将最优策略记入 ,。
- 以此类推,直到求出 个矩阵相乘的最优值 。
(4)构造最优解
上面得到的最优值只是矩阵连乘的最小的乘法次数,并不知道加括号的次序,需要从记录表中还原加括号次序,构造出最优解。
用二维数组 来存放各个子问题的最优决策(加括号的位置)。
根据最优决策信息数组 递归构造最优解。 表示 最优解的加括号位置,即 ,我们再递归构造两个子问题 、 的最优解加括号位置,一直递归到子问题只包含一个矩阵为止。
【算法实现】
#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;
const int msize = 100;
int p[msize];
int m[msize][msize];
int s[msize][msize];
int n;
void matrixchain()
{
memset(m, 0, sizeof(m));
memset(s, 0, sizeof(s));
for(int len = 2; len <= n; len++) // 不同规模的子问题
{
for(int l = 1; l + len - 1 <= n; l++)
{
int r = l + len - 1;
m[l][r] = m[l+1][r] + p[l-1] * p[l] * p[r]; // 决策为k=l的乘法次数
s[l][r] = l; // 子问题的最优策略是l;
for(int k = l + 1; k < r; k++) // 对从l到r的所有决策,求最优值,记录最优策略
{
int t = m[l][k] + m[k+1][r] + p[l-1] * p[k] * p[r];
if(t < m[l][r])
{
m[l][r] = t;
s[l][r] = k;
}
}
}
}
}
void print(int i, int j)
{
if(i == j)
{
cout << "A[" << i << "]";
return ;
}
cout << "(";
print(i, s[i][j]);
print(s[i][j] + 1, j);
cout << ")";
}
int main()
{
cout << "请输入矩阵的个数 n:";
cin >> n;
cout << "请依次输入每个矩阵的行数和最后一个矩阵的列数:";
for(int i = 0; i <= n; i++) cin >> p[i];
matrixchain();
print(1, n);
cout << endl;
/*
for(int i = 1; i <= n; i++) // 用于测试
{
for(int j = i; j <= n; j++) cout << m[i][j] << " ";
cout << endl;
}
for(int i = 1; i <= n; i++)
{
for(int j = i; j <= n; j++) cout << s[i][j] << " ";
cout << endl;
}
cout << endl;
*/
cout << "最小计算量的值为 " << m[1][n] << endl;
}
样例图解如下:
初始化:
计算 个矩阵相乘的最优值
计算 个矩阵相乘的最优值
计算 个矩阵相乘的最优值
计算 个矩阵相乘的最优值
构造最优解
【复杂度分析】
(1)时间复杂度:由程序可以得出,语句 ,它是算法的基本语句,在 层 for 循环中嵌套。最坏情况下,该语句的执行次数为 ,print() 函数算法的时间主要取决于递归,时间复杂度为 。故该程序的时间复杂度为 。
(2)空间复杂度:该程序的输入数据的数组为 ,辅助变量为 、、、、、、,空间复杂度取决于辅助空间,因此空间复杂度为 。