动态规划——矩阵连乘问题
题目: 给定n个矩阵{A1,A2,…,An}(n<=40),其中Ai与Ai+1是可乘的,i=1,2…,n。第i个矩阵的维数用pi−1,pi来表示。如何确
定计算矩阵连乘积的计算次序,使得依此次序计算矩阵连乘积需要的数乘次数最少。例如,给定三个连乘矩阵{A1,A2,A3}的维数
数组p为:10,100,5,50,即分别是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次。
输入示例:
6
30 35 15 5 10 20 25
输出示例:
15125
解题思路分析:
矩阵相乘最少应该有两个矩阵相乘,一个矩阵是不存在相乘的问题的。那最多有多少个矩阵相乘呢?最多可以有n个矩阵相乘,至于n是多少我们也不知道。
题目也是让我们求n个矩阵相乘的问题,我们可以将n个矩阵相乘看成两个大矩阵相乘,这里的大并不是指矩阵真的很大,文字不好描述,我举例说明一下:
例如求n个矩阵 (A1,A2,A3,…,An-1,An)相乘,我们可以将其看成矩阵(A1,A2,…,Ak)与矩阵(Ak+1,Ak+2,…,An)相乘,这里k的范围为:1<k<n。同地我们还可以将矩阵(A1,A2,…,Ak)与矩阵(Ak+1,Ak+2,…,An)又进一步细分。
像这样把求原问题的解转化为求子问题的解,我们可以考虑用动态规划来实现。
动态规划的解题步骤:
-
明确目的状态:我们要求的目的状态是什么,就本题而言,我们的目的状态是求n个矩阵相乘的最小乘法次数
-
明确dp数组的含义:dp数组是用来存放每一个状态下的值,具体题型具体分析,你要明白dp数组里面的值是什么意思,就本题而言,dp[i] [j]是用来存放矩阵(Ai,Ai+1,…,Aj)的最小相乘次数
-
建立状态转移方程:每一个状态之间的关系,目的状态与它的上一个状态之间的关系,本题的状态转移方程为:
d p [ i ] [ j ] = m i n d p [ i ] [ k ] + d p [ k + 1 ] [ j ] + p [ i − 1 ] p [ k ] p [ j ] ( i < j , i < = k < j ) dp[i][j]=min{ dp[i][k]+dp[k+1][j]+p[i-1]p[k]p[j] }(i<j , i<=k<j) dp[i][j]=mindp[i][k]+dp[k+1][j]+p[i−1]p[k]p[j](i<j,i<=k<j)
解释:为什么要加上min,因为 i 到 j 之间 k 的取值有多个,k 的取值不同,产生的dp[i] [j] 的值不同,我们要求最小的相乘次数,所以要取最小的dp[i] [j]的值
整个动态规划解题步骤中,最关键最核心的是状态转移方程,那么状态转移方程是怎么来的呢?
状态转移方程是我们经过探讨、总结得出来的——目的状态与它的上一次状态的关系表达式
探讨过程:
假设n为3,求矩阵(A1, A2, A3)相乘的最小相乘次数,并且假设(A1A2)A3 这个相乘的次序所用的乘法次数最少,则在目的转态n为3下的最少相乘次数为p[0] * p[1] * p[2] +p[0] * p[2] * p[3] (p数组存放的是矩阵的维数,例A1的行为p[0] 列为p[1])
而根据我们的定义dp数组的定义,则有dp[1] [3] = p[0] * p[1] * p[2] +p[0] * p[2] * p[3] ;
因为(A1A2)A3 这个相乘的次序所用的乘法次数最少,所以矩阵A1A2相乘的次序所用的乘法次数也必然最少
则dp[1] [3] = dp[1] [2] +p[0] * p[2] * p[3] ①式
(p[0]是矩阵A1的行数,p[2],p[3]分别为矩阵A3的行数和列数)
设n为4,求矩阵(A1, A2, A3,A4)相乘的最小相乘次数,我们可以将其看成矩阵(A1, A2, A3)与矩阵A4相乘的最小相乘次数,也假设这样相乘是乘法次数最少的,则
目的状态为n=4的最小次数等于 目的状态为n=3的矩阵与矩阵A4相乘的最小次数 ,根据dp数组的含义,矩阵(A1, A2, A3)的最小相乘
次数为dp[1] [3], 根据上面对n=3的推导,dp[1] [4] = dp[1] [3] +p[0] * p[3] * p[4] ②式
又因为矩阵相乘最少应该有两个矩阵相乘,且根据dp数组的含义有:
如果 i = j, 则 dp[i] [j] =0;
对①②式变型有:
dp[1] [3] = dp[1] [2] + dp[3] [3] + p[0] * p[2] * p[3]
dp[1] [4] = dp[1] [3] + dp[4] [4] +p[0] * p[3] * p[4]
再多讨论一下n=5,n=6的情况,就可以得到我们的状态转移方程:
d
p
[
i
]
[
j
]
=
m
i
n
d
p
[
i
]
[
k
]
+
d
p
[
k
+
1
]
[
j
]
+
p
[
i
−
1
]
p
[
k
]
p
[
j
]
(
i
<
j
,
i
<
=
k
<
j
)
dp[i][j]=min{ dp[i][k]+dp[k+1][j]+p[i-1]p[k]p[j] }(i<j , i<=k<j)
dp[i][j]=mindp[i][k]+dp[k+1][j]+p[i−1]p[k]p[j](i<j,i<=k<j)
代码示例:
#include<bits/stdc++.h>
using namespace std;
int n;//n为矩阵的个数
int p[41];//用来存放矩阵的行数和列数,例如矩阵Ai的行数为p[i-1]列数为p[i]
int func();// 返回矩阵相乘的最小次数
int main(){
cin>>n;
//因为矩阵有n个,所以p数组中应该有n+1个值
for(int i=0;i<=n;i++){
cin>>p[i];
}
int num=func();
cout<<num<<endl;
}
int func(){
//vector容器 dp[n+1][n+1]相当于动态的二维数组,
//根据输入的n的值来自动开辟空间 ,其类型为int
vector< vector<int> > dp(n+1,vector<int>(n+1));
for(int i=n;i>=1;i--){
for(int j=i;j<=n;j++){
if(i==j){
dp[i][j]=0;
}
if(i<j){
// 因为是求最小值,所以尽可能的将temp设得大一点
int temp=1000000;
for(int k=i;k<j;k++){
int ans=dp[i][k]+dp[k+1][j]+p[i-1]*p[k]*p[j];
if(ans<temp){
temp=ans;
}
}
dp[i][j]=temp;
}
}
}
return dp[1][n];
}
为方便理解,大家可以画一个n*n的方格,以n为4为例:
表格(1,2)表示矩阵A1A2相乘的最小次数:dp[1] [2]
表格(1,4)表示矩阵{A1A2A3A4}相乘的最小次数:dp[1] [4]
A1 | A2 | A3 | A4 | |
---|---|---|---|---|
A1 | 0 | dp[1] [2] | dp[1] [3] | dp[1] [4] |
A2 | 0 | dp[2] [3] | dp[2] [4] | |
A3 | 0 | dp[3] [4] | ||
A4 | 0 |
根据状态转移方程,dp[1] [2] =dp[1] [1] +dp [2] [2] + p[0] * p[1] * p[2]
像这样的每一个目的状态都可以用上一个目的状态来表式
表格中只有在对角线的右边,才是具有实际意义的dp数组
如果在下面,例如dp[2] [1] 表示矩阵{A2A1}相乘的最小次数,A2A1改变了矩阵相乘的顺序,是不对的
根据矩阵知识A1A2 不等于 A2A1