前言
从旭东的博客 看到一篇博文:矩阵连乘最优结合 动态规划求解,挺有意思的,这里做个转载【略改动】。
问题
矩阵乘法满足结合律,但不满足交换律。例如矩阵$A_{ab}, B_{bc}, C_{cd}$ 连乘得到矩阵$S_{ad}$
\[ S_{ad}=A_{ab} B_{bc} C_{cd} \]
实际运算可以先将矩阵A和B相乘,S=(AB)C,也可以先将矩阵B和C相乘,S=A(BC) 。这两种算法的计算量是否一样呢?
先看A×B的计算量。A×B是a行c列矩阵,共a×c个元素。b是A、B两矩阵的乘法接口,积矩阵的每个元素都需经过b次乘法运算。那么A×B需要a×b×c次乘法运算。
(AB)C乘法运算次数为:abc+acd=ac(b+d)
A(BC)乘法运算次数为:bcd+abd=bd(a+c)
由于a、b、c、d的取值是任意的, 所以ac(b+d)和bd(a+c)不可能保持相同,也即结合律可以用于矩阵乘法优化。那么,对于连乘AiAi+1…Aj,如何找出最优的运算顺序?
分析
这个问题长得像递归,但是用递归似乎不好做,因为子问题里面有很多分支,那子问题的返回值是什么,你返回哪个分支的返回值?而且就算能实现,肯定会对一些相同的子问题重复计算。
由于计算n个矩阵的连乘,总是通过计算更少个数的矩阵连乘实现的,最终化归到计算两个矩阵的相乘,所以考虑从两个矩阵相乘着手。这样自底向上,就把问题解决了。当我们把所有的两个矩阵相乘的乘法运算次数求出后,便可以去计算三个矩阵相乘的乘法运算次数。这时,三个矩阵的连乘相当于两个矩阵相乘。这样,所有的矩阵连乘都是两个矩阵相乘,其乘法运算次数为两个矩阵各自的乘法运算次数之和再加上两个矩阵相乘的乘法运算次数。对于两个矩阵A、B相乘的乘法运算次数$\beta$,上面A×B的示例已经讲过了。进一步可以抽象为
\[\beta=行*接口*列\]
C++提供了vector模板类,作为数组的一个替代品,我们用vector实现n阶方阵。首先确定跨度,再确定起点,构成一个二级嵌套循环,通过起点的平移确定子式。再嵌套一级遍历子式的分割点求出子式的最少计算次数。最后我们得到最大的子式,也就是连乘本身的最少运算次数及分割方案。
代码如下:
1 #include2 #include 3 #include 4 #include 5 #include <string> 6 7 using namespace std; 8 9 //计算矩阵的分割方案(基于向量) 10 int calculate_M(vector int> >&num, vector int, int> > &data, vector int> > &points) { 11 int matrixNum = data.size(); 12 int span;//起止点间隔距离,表示跨度 13 int start;//起点 14 int end;//终点 15 int spiltPoint;//分割点 16 int multiplyTimes;//临时存储子式的乘法运算次数 17 for (span = 1; span < matrixNum; span++) { ///间隔距离 相邻矩阵的间隔距离为1 18 for (start = 0; start < matrixNum - span; start++) { ///起点平移 19 //子式确立,下面开始计算这个子式的最少乘法运算次数,初值先设为最大整数 20 end = start + span; 21 num[start][end] = INT_MAX; 22 for (spiltPoint = start; spiltPoint < end; spiltPoint++) { 23 24 multiplyTimes = num[start][spiltPoint] + num[spiltPoint + 1][end] + data[start].first*data[spiltPoint].second*data[end].second; 25 if (multiplyTimes < num[start][end]) { 26 points[start][end] = spiltPoint; ///记录分割点 27 num[start][end] = multiplyTimes; ///记录最少乘法次数 28 } 29 } 30 } 31 } 32 return 0; 33 } 34 35 //根据记录的分割点,生成最后的矩阵相乘表达式 36 string make_result(vector int> > &points, int t1, int t2) { 37 if (t1 == t2) 38 return string(1, 'A' + t1); 39 int split = points[t1][t2]; 40 return "(" + make_result(points, t1, split) + "*" + make_result(points, split + 1, t2) + ")"; 41 } 42 43 44 int main() 45 { 46 int matrixNum=0;///连乘的矩阵个数 47 vector int, int>> data; ///存储矩阵的尺寸 48 49 //固定输入 50 data.push_back(make_pair(10, 100)); //A 51 data.push_back(make_pair(100, 5)); //B 52 data.push_back(make_pair(5, 25)); //C 53 data.push_back(make_pair(25, 15)); //D 54 data.push_back(make_pair(15, 20)); //E 55 matrixNum = 5; 56 57 //为记录乘法次数和分割点的n阶矩阵分配空间并初始化 58 vector int> > num(matrixNum, vector<int>(matrixNum)); 59 vector int> > points(matrixNum, vector<int>(matrixNum)); 60 for (int i = 0; i < matrixNum; i++) { 61 for (int j = 0; j < matrixNum; j++) { 62 points[i][j] = -1; 63 num[i][j] = 0; 64 } 65 } 66 67 calculate_M(num, data, points); 68 cout << make_result(points, 0, matrixNum - 1) << "\t最少乘法次数为:" << num[0][matrixNum - 1] << endl; 69 70 cin >> matrixNum; 71 return 0; 72 }
代码输出为:
((A*B)*((C*D)*E)) 最少乘法次数为:9375 |
如果从控制台输入矩阵尺寸的话,可以输入第一个矩阵的行数和各矩阵的列数。直接使用这个序列存储矩阵尺寸。
1 #include2 #include 3 #include 4 #include 5 #include <string> 6 7 using namespace std; 8 9 int* uncertainInput(int& inputNum); 10 11 //计算矩阵的分割方案(基于数组) 12 int calculate_M1(int*data, int matrixNum, vector int> >&num, vector int> > &points) { 13 14 int span;//起止点间隔距离,表示跨度 最大跨度比总的矩阵数小1 15 int start;//起点 16 int end;//终点 17 int spiltPoint;//分割点 分割线在分割点与下一点之间 18 int multiplyTimes;//临时存储子式的乘法运算次数 19 int rows, columns, interfaces;//行 列 接口 20 21 for (span = 1; span < matrixNum-1; span++) { ///间隔距离 相邻矩阵的间隔距离为1 22 for (start = 1; start + span < matrixNum; start++) { ///起点从第一个矩阵的列开始 23 //子式确立,下面开始计算这个子式的最少乘法运算次数,初值先设为最大整数 24 end = start + span; 25 num[start][end] = INT_MAX; 26 for (spiltPoint = start; spiltPoint < end; spiltPoint++) { 27 rows = *(data + start-1); 28 columns = *(data + end); 29 interfaces = *(data + spiltPoint); 30 31 multiplyTimes = num[start][spiltPoint] + num[spiltPoint + 1][end] + rows * interfaces * columns; 32 if (multiplyTimes < num[start][end]) { 33 points[start][end] = spiltPoint; ///记录分割点 34 num[start][end] = multiplyTimes; ///记录最少乘法次数 35 } 36 } 37 } 38 } 39 return 0; 40 } 41 42 //根据记录的分割点,生成最后的矩阵相乘表达式 43 string make_result(vector int> > &points, int t1, int t2) { 44 if (t1 == t2) 45 return string(1, 'A' + t1 -1); 46 int split = points[t1][t2]; 47 return "(" + make_result(points, t1, split) + "*" + make_result(points, split + 1, t2) + ")"; 48 } 49 50 51 int main() 52 { 53 int matrixNum = 0;///连乘的矩阵个数 54 vector int, int>> data; ///存储矩阵的尺寸 55 56 //手动输入 57 //输入的第一个值是第一个矩阵的行,其余是各矩阵的列 58 int* matrixSizeSeq; 59 matrixSizeSeq = uncertainInput(matrixNum); 60 if (matrixSizeSeq == NULL) 61 return false; 62 63 //重构为向量 64 /*for (int ptrOffset = 0; ptrOffset < matrixNum - 1; ptrOffset++) ///遍历到倒数第二个输入 65 data.push_back(make_pair(*(matrixSizeSeq + ptrOffset), *(matrixSizeSeq + ptrOffset + 1)));*/ 66 67 //为记录乘法次数和分割点的n阶矩阵分配空间并初始化 68 vector int> > num(matrixNum, vector<int>(matrixNum)); 69 vector int> > points(matrixNum, vector<int>(matrixNum)); 70 for (int i = 0; i < matrixNum; i++) { 71 for (int j = 0; j < matrixNum; j++) { 72 points[i][j] = -1; 73 num[i][j] = 0; 74 } 75 } 76 77 calculate_M1(matrixSizeSeq, matrixNum, num, points); 78 cout << make_result(points, 1, matrixNum - 1) << "\t最少乘法次数为:" << num[1][matrixNum - 1] << endl; 79 cin >> matrixNum; 80 return 0; 81 } 82 83 int* uncertainInput(int& inputNum) 84 { 85 //输入0表示输入完成 输入负数认为手误 86 using namespace std; 87 88 int allocateSpace = 5; 89 int* inputSequence = (int *)malloc(allocateSpace * sizeof(int)); ///存储矩阵的尺寸输入 90 91 92 int ptrOffset = 0; ///指针偏移 93 int tempSize = 0; ///临时存储输入值 94 95 do { 96 cin >> tempSize; 97 if (tempSize <= 0) 98 continue; 99 *(inputSequence + ptrOffset++) = tempSize; 100 if (ptrOffset >= allocateSpace) 101 { 102 allocateSpace += 5; 103 inputSequence = (int*)realloc(inputSequence, allocateSpace * sizeof(int)); 104 } 105 } while (tempSize != 0); 106 inputNum = ptrOffset; ///偏移量即输入个数 107 return inputSequence; 108 }
比如求$S_{ad}$,则在控制台输入a b c d 0,然后回车即可。当然 a b c d 必须替换为数字。
代码中用到的一些知识
C++提供模版类string,其中一个构造方法可将字符转化为字符串。如 string(1, 'A'+1),第一个参数是源字符延拓次数,这个构造函数将‘B’转化为"B"。