问题描述
给定n个矩阵{A1,A2,…,An},其中Ai与Ai+1是可乘的,i=1,2…,n-1。如何确定计算矩阵连乘积的计算次序,使得依此次序计算矩阵连乘积需要的数乘次数最少。
由于矩阵乘法满足结合律
,所以计算矩阵的连乘可以有许多不同的计算次序。这种计算次序可以用加括号的方式来确定。
若一个矩阵连乘积的计算次序完全确定,即连乘积已完全加括号
,则可以依此次序反复调用2个矩阵相乘的标准算法计算矩阵连乘积.
例如,给定三个连乘矩阵{A1,A2,A3}的维数分别是10*100,100*5和5*50,采用((A1A2)A3),乘法次数为10*100*5+10*5*50=7500次,而采用(A1(A2A3)),乘法次数为100*5 *50+10*100*50 = 75000次乘法,显然,最好的次序是((A1A2)A3),乘法次数为7500次。
问题分析
矩阵链乘法问题描述:
给定由n个矩阵构成的序列{A1,A2,…,An},对乘积A1A2…An,找到最小化乘法次数的加括号方法。
1)寻找最优子结构
此问题最难的地方在于找到最优子结构。对乘积A1A2…An的任意加括号方法都会将序列在某个地方分成两部分,也就是最后一次乘法计算的地方,我们将这个位置记为k,也就是说首先计算
A1...Ak
和Ak+1...An
,然后再将这两部分的结果相乘。
最优子结构如下:假设A1A2…An的一个最优加括号把乘积在Ak和Ak+1间分开,则前缀子链A1…Ak的加括号方式必定为A1…Ak的一个最优加括号,后缀子链同理。矩阵连乘计算次序问题的最优解包含着其子问题的最优解。这种性质称为最优子结构性质
。
一开始并不知道k的确切位置,需要遍历所有位置以保证找到合适的k来分割乘积
。
2)构造递归解
设m[i,j]为矩阵链Ai…Aj的最优解的代价。A[i:j]表示 A i A_i Ai A i + 1 A_{i+1} Ai+1… A j A_j Aj
- 设计算A[i:j],1≤i≤j≤n,所需要的最少数乘次数为
m[i,j]
,则原问题的最优值为m[1,n] - 当
i = j
时,A[i:j]=Ai,因此,m[i,i]=0
,i=1,2,…,n - 当
i < j
时,若A[i:j]的最优次序在Ak和Ak+1间断开,则
可以递归地定义m[i,j]为:
3)构建辅助表,解决重叠子问题
从第二步的递归式可以发现解的过程中会有很多重叠子问题,可以用一个nXn维的辅助表m[n][n] 和 s[n][n],分别表示最优乘积代价
及其分割位置k
。
辅助表s[n][n]可以由2种方法构造:
- 一种是
自底向上填表构建
,该方法要求按照递增的方式逐步填写子问题的解,也就是先计算长度为2的所有矩阵链的解,然后计算长度3的矩阵链,直到长度n; - 另一种是
自顶向下填表的备忘录法
,该方法将表的每个元素初始化为某特殊值(本问题中可以将最优乘积代价设置为一极大值),以表示待计算,在递归的过程中逐个填入遇到的子问题的解。
对于一组矩阵:A1(30x35),A2(35x15),A3(15x5),A4(5x10),A5(10x20),A6(20x25) 个数N为6
那么p数组保存它们的行数和列数:p={30,35,15,5,10,20,25}共有N+1即7个元素
p[0],p[1]代表第一个矩阵的行数和列数,p[1],p[2]代表第二个矩阵的行数和列数…p[5],p[6]代表第六个矩阵的行数和列数
计算顺序为:
辅助表m: m[i][j]代表从矩阵Ai,Ai+1,Ai+2…直到矩阵Aj最小的相乘次数,比如m[2][5]代表A2A3A4A5最小的相乘次数,即最优的乘积代价。
我们看上图,从矩阵A2到A5有三种断链方式:A2{A3A4A5}、{A2A3}{A4A5}、{A2A3A4}A5,这三种断链方式会影响最终矩阵相乘的计算次数,我们分别算出来,然后选一个最小的,就是m[2][5]的值,同时保留断开的位置k在s数组中。
复杂度分析
算法matrixChain的主要计算量取决于算法中对r,i和k的3重循环。循环体内的计算量为O(1),而3重循环的总次数为O(n3)。因此算法的计算时间上界为
O(n3)。算法所占用的空间显然为
O(n2)
核心代码(自底向上的方式)
代码(自底向上的方式)
#include<iostream>
using namespace std;
const int N=7;
//p为矩阵链,p[0],p[1]代表第一个矩阵的行数和列数,p[1],p[2]代表第二个矩阵的行数和列数......length为p的长度
//所以如果有六个矩阵,length=7,m为存储最优结果的二维矩阵,s为存储选择最优结果路线的
//二维矩阵
void MatrixChainOrder(int *p,int m[N][N],int s[N][N],int length)
{
int n=length-1;
int l,i,j,k,q=0;
//m[i][i]只有一个矩阵,所以相乘次数为0,即m[i][i]=0;
for(i=1;i<length;i++)
{
m[i][i]=0;
}
//l表示矩阵链的长度
// l=2时,计算 m[i,i+1],i=1,2,...,n-1 (长度l=2的链的最小代价)
for(l=2;l<=n;l++)
{
for(i=1;i<=n-l+1;i++)
{
j=i+l-1; //以i为起始位置,j为长度为l的链的末位,
m[i][j]=0x7fffffff;
//k从i到j-1,以k为位置划分
for(k=i;k<=j-1;k++)
{
q=m[i][k]+m[k+1][j]+p[i-1]*p[k]*p[j];
if(q<m[i][j])
{
m[i][j]=q;
s[i][j]=k;
}
}
}
}
cout << m[1][N-1] << endl;
}
void PrintAnswer(int s[N][N],int i,int j)
{
if(i==j)
{
cout<<"A"<<i;
}
else
{
cout<<"(";
PrintAnswer(s,i,s[i][j]);
PrintAnswer(s,s[i][j]+1,j);
cout<<")";
}
}
int main()
{
int p[N]={30,35,15,5,10,20,25};
int m[N][N],s[N][N];
MatrixChainOrder(p,m,s,N);
PrintAnswer(s,1,N-1);
return 0;
}
动态规划算法的基本要素
两个基本要素(重要性质): 最优子结构性质
和子问题重叠性质
.
最优子结构性质
矩阵连乘计算次序问题的最优解包含着其子问题的最优解。这种性质称为最优子结构性质
。
分析方法: 在分析问题的最优子结构性质时,所用的方法具有普遍性:首先假设由问题的最优解导出的子问题的解不是最优的,然后再设法说明在这个假设下可构造出比原问题最优解更好的解,从而导致矛盾。
利用问题的最优子结构性质,以自底向上的方式递归地从子问题的最优解逐步构造出整个问题的最优解。最优子结构是问题能用动态规划算法求解的前提。
重叠子问题
递归算法求解问题时,每次产生的子问题并不总是新问题,有些子问题被反复计算多次。这种性质称为子问题的重叠性质
。
动态规划算法,对每一个子问题只解一次,而后将其解保存在一个表格中
,当再次需要解此子问题时,只是简单地用常数时间查看一下结果
。
通常不同的子问题
个数随问题的大小呈多项式增长
。因此用动态规划算法只需要多项式时间,从而获得较高的解题效率。
产生重叠子问题(递归算法)
int LookupChain(int i, int j){
if(i == j)
return 0;
m[i][j] = LookupChain(i,i) + LookupChain(i+1,j)+p[i-1]*p[i]*p[j];
s[i][j] = i;
for(int k=i+1; k<j;k++){
int t = LookupChain(i,k)+LookupChain(k+1,j)+p[i-1]*p[k]*p[j];
if(t<m[i][j]){
m[i][j]=t;
s[i][j]=k;
}
}
return m[i][j];
}
复杂度分析
递归算法计算时间: Ω \Omega Ω(2n)
动态规划算法计算时间:O(n3)
备忘录方法(自顶向下)
基本概念
核心算法
备忘记录项m[i][j]个数: O(n2)
每个记录项m[i][j]填入时间: O(n)
算法时间复杂度:
O(n3)
算法
#include<iostream>
using namespace std;
#define N 7 //N为7,实际表示有6个矩阵
int p[N]={30,35,15,5,10,20,25};
int m[N][N],s[N][N];
int LookupChain(int i, int j){
if(m[i][j]>0)
return m[i][j];
if(i == j)
return 0;
m[i][j] = LookupChain(i,i) + LookupChain(i+1,j)+p[i-1]*p[i]*p[j];
s[i][j] = i;
for(int k=i+1; k<j;k++){
int t = LookupChain(i,k)+LookupChain(k+1,j)+p[i-1]*p[k]*p[j];
if(t<m[i][j]){
m[i][j]=t;
s[i][j]=k;
}
}
return m[i][j];
}
int MemorizedMatrixChain(int n, int m[][N], int s[][N]){
for(int i=1;i<=n;i++){ //初始化默认都是0
for(int j=1;j<=n;j++)
m[i][j] = 0;
}
return LookupChain(1,n);
}
/*
*追踪函数:根据输入的i,j限定需要获取的矩阵链的始末位置,s存储断链点
*/
void Traceback(int i,int j, int s[][N]){
if(i==j) //回归条件
{
cout<<"A"<<i;
}
else //按照最佳断点一分为二,接着继续递归
{
cout<<"(";
Traceback(i,s[i][j],s);
Traceback(s[i][j]+1,j,s);
cout<<")";
}
}
int main(){
MemorizedMatrixChain(N-1,m,s);//N-1因为只有六个矩阵
Traceback(1,6,s);
return 0;
}
动态规划与备忘录方法对比总结
- 矩阵连乘积的最优计算次序问题可用
自顶向下的备忘录算法
或者自底向上的动态规划算法
在O(n3)计算时间内求解,这两个算法都利用了子问题重叠性质,共有θ(n2)个不同的子问题。 - 对于每个子问题,两种方法都只解一次,并记录答案。再次遇到该子问题时,不重新求解而简单地取用已经得到的答案,节省了计算量,提高了算法效率。
一般来讲,当一个问题的所有子问题都至少需要解一次时,用动态规划算法比用备忘录方法好
。此时,动态规划算法没有多余计算。同时,对于许多问题,常可利用其规则的表格存取方式,减少动态规划算法的计算时间和空间需求。 当子问题空间中的部分子问题可不必求解时,用备忘录方法较为有利
,因为该方法只解那些确实需求求解的子问题。