题目:
拿到这道题,我们首先要知道矩阵相乘的规则和过程,具体的乘法过程如下:
要求A的列数=B的行数,并且结果的C矩阵的元素值满足如下关系:
从上式中我们可以求得矩阵C中每个元素值,但是我们这题关心的并不是值,而是相乘的此时,这里我们可以看到求每个元素,需要m个乘积的和,这样的话,求取完整的矩阵C需要进行l*m*m次相乘。同时由于相邻矩阵满足前一个列数等于后一个行数的关系,这样,我们把每个矩阵行列数保存在数组p中,并且相邻矩阵的相等行列书只存储一次,这样的话,对于矩阵M[i](i=1,2,3,.;.....n),其是p[i-1]行p[i]列的矩阵。
确定好上述的数据存储,接下来考虑本题,如果检查每种不同的计算顺序那么福复杂度高达O(n!),显然是不靠谱的,这样我们可不可以考虑使用动态规划呢?
答案是可以的,我们对于一个大矩阵M[i]*M[i+1]*...*M[j],我们不考虑过多,只想在得到这个矩阵的前一步乘积的情况,有哪些情况呢?比如对于M1*M2*M3*M4*M5,显然的我们有以下几种情况,并由此获得完整的M1*M2*M3*M4*M5的公式:
1.(M1)*(M2*M3*M4*M5)的次数 =(M1)的次数+ (M2*M3*M4*M5)的次数+p[0]*p[1]*p[5]
2.(M1*M2)*(M3*M4*M5)的次数=(M1*M2)的次数+(M3*M4*M5)的次数+p[0]*p[2]*p[5]
3.(M1*M2*M3)*(M4*M5)的次数=(M1*M2*M3)的次数+(M4*M5)的次数+p[0]*p[3]*p[5]
4.(M1*M2*M3*M4)*(M5)的次数=(M1*M2*M3*M4)的次数+(M5)的次数+p[0]*p[4]*p[5]
其中单个矩阵相乘的次数显然为0,我们再取其中求得最小的次数作为最终M1*M2*M3*M4*M5最小链乘次数.这样的话,我们其实就可以很简单的获得动态规划下数组的意义和递推关系式,dp[i][j];表示下标从i~j的这部分矩阵的最小链乘次数,其中矩阵下标从1到n,递推关系式如下:
dp[i][j]=0 (i=j)
=min{dp[i][k]+dp[k+1][j]+p[i-1]*p[k]*p[j]} (其中i<=m<=j) (i<j)
这样就和容易得到下面得解题代码:
#include<iostream>#include<algorithm>using namespace std;#define Max_N 100+1int p[Max_N]; //存放各个矩阵的行列数 ,第i个矩阵大小为p[i-1]*p[i] int dp[Max_N][Max_N]; //矩阵下标从1~n,dp[i][j]表示下标从i~j的这部分矩阵的最小链乘次数 int main(){ int n=6; cin>>n; for(int i=1;i<=n;i++) { cin>>p[i-1]>>p[i]; dp[i][i]=0; } //int p[]={30,35,15,5,10,20,25}; //测试使用 //cout<<endl; for(int l=2;l<=n;l++) { for(int i=1;i<=n-l+1;i++) { int j=i+l-1; dp[i][j]=999999999; for(int k=i;k<j;k++) { //测试使用 //cout<<"dp["<<i<<"]["<<k<<"]:"<<dp[i][k]<<"-"; //cout<<"dp["<<k+1<<"]["<<j<<"]:"<<dp[k+1][j]<<"-"; //cout<<p[i-1]*p[k]*p[j]<<endl; dp[i][j]=min(dp[i][j],dp[i][k]+dp[k+1][j]+p[i-1]*p[k]*p[j]); //cout<<"dp["<<i<<"]["<<j<<"]:"<<dp[i][j]<<endl; } //cout<<"------------"<<endl; } } cout<<dp[1][n]<<endl; return 0;} 但是到这并不结束,下面我想说的是在解题时我的一个错误,并分析给大家,以此借鉴:接下来是我的源码:#include<iostream>#include<algorithm> //注意,该方法是错误方法 using namespace std;#define Max_N 100+1//int p[Max_N]; //存放各个矩阵的行列数 ,第i个矩阵大小为p[i-1]*p[i] int dp[Max_N][Max_N]; //矩阵下标从1~n,dp[i][j]表示下标从i~j的这部分矩阵的最小链乘次数 int main(){ int n=6; //cin>>n; for(int i=1;i<=n;i++) { //cin>>p[i-1]>>p[i]; dp[i][i]=0; } int p[]={30,35,15,5,10,20,25}; for(int i=0;i<=n;i++) { cout<<p[i]<<" "; } cout<<endl; for(int i=1;i<=n;i++) { for(int j=i+1;j<=n;j++) { dp[i][j]=99999999; for(int mid=i;mid<j;mid++) //分成两个矩阵相乘,i~k和k+1~j { //下标i~k的矩阵相乘结果为大小为p[i-1]*p[mid]的矩阵 //下标k+1~j的矩阵相乘结果为大小为p[mid]*p[j]的矩阵 //最后两者矩阵相乘,链乘次数为p[i-1]*p[mid]*p[j] //cout<<"dp["<<i<<"]["<<mid<<"]:"<<dp[i][mid]<<"-"; //cout<<"dp["<<mid+1<<"]["<<j<<"]:"<<dp[mid+1][j]<<"-"; //cout<<p[i-1]*p[mid]*p[j]<<endl; dp[i][j]=min(dp[i][j],dp[i][mid]+dp[mid+1][j]+p[i-1]*p[mid]*p[j]); //cout<<"dp["<<i<<"]["<<j<<"]:"<<dp[i][j]<<endl; } //cout<<"------------"<<endl; } } for(int i=1;i<=n;i++) { for(int j=i;j<=n;j++) { cout<<"dp["<<i<<"]["<<j<<"]:"<<dp[i][j]<<endl; } } cout<<dp[1][n]<<endl; return 0;} 我将示例中的测试案例写在了程序中,便于每次的调试。上述代码的问题就在于其循环时的变量范围,虽然说两种代码最终都保证dp[i][j]中的下标范围从1变化到n,但是第二种的方法就错在一下:该方法的作物之处就在于i,j下标是不断依次增大的,这样的话在进行下面一句话:
dp[i][j]=min(dp[i][j],dp[i][mid]+dp[mid+1][j]+p[i-1]*p[mid]*p[j]) 时, 在 dp[mid+1][j]中mid+1>i的情况下,用 dp[mid+1][j]来求dp[i][j],相当于用之后遍历到的来求之前的数据,这样在未遍历到dp[mid+1][j]时,其值恒为0,也就是说从mid+1到j矩阵链乘次数为0,这样显然是不可能的。所以为了避免这种情况,首先要想到对于多个矩阵的相乘,其肯定是由其小部分的矩阵链乘结果得到的,那么就不能按照下标从小到大顺序遍历,那么就应该用下标范围的长度来作为遍历,也就是说先遍历完短范围的矩阵链乘结果,再得到大范围链乘结果,抛开一句,这里之所以不能用i,j下标作为遍历标准,就是因为其求结果的时候,并不是说先求小坐标的矩阵,而是先求小范围,也就是长度小的范围内,矩阵的链乘结果,比如,应该先求 M5*M6*M7,之后再去求M2*M3*...M7,其实就是因为后者的求取需要其一部分矩阵相乘的结果 。
其实这题就告诉我们,动态规划解题时,我们要找准递推式中前后变量的范围,并在保证后者求出结果后再进行递推,这样就要求我们在保证动态数组变量范围不变的情况下,找到正确的下标变化过程。
PS:看书,解题越多,并且感悟越多,对于提升能力肯定是有帮助的,不能只是机械地敲书上的代码~
新闻热点
疑难解答