前提:
输入矩阵的个数,和各个维度,保证Ai和Ai+1是可乘的(相邻之间可乘),求输出的矩阵相乘顺序,和相乘次数,使相乘次数最小。
eg:输入
5
10 1 50 50 20 5(存到p[0:5]中)
第i个矩阵的行、列分别是p[i-1],p[i]
分析:
1.分析最优结构:
特征:计算A[i:j]的最优次序所包含的计算矩阵子链 A[i:k]和A[k+1:j]的次序也是最优的。
矩阵连乘计算次序问题的最优解包含着其子问题的最优解。这种性质称为最优子结构性质。问题的最优子结构性质是该问题可用动态规划算法求解的显著特征。
2.建立递归关系:
对于A[i:j],我们要求的是A[i:k]+A[k+1:j]+p[i-1]*p[k]*p[j];
p[i-1]*p[k]*p[j]是前两个矩阵相乘的情况,这个我们可以证明“分析最优结构时”,“若A[i:j]是最优的则它划分的子链A[i:k],A[k+1,j]也是最优的了”
反正法:若A[i:k]还有更优的情况,那么p[i-1]*p[k]*p[j]的值也会更小,此时,A[i:j]就不是最优的啦。
我们的数学模型就建立好了
对于非平凡问题(不简单问题):A[i:j]=A[i:k]+A[k+1:j]+p[i-1]*p[k]*p[j]
对于平凡问题(简单问题):若i==j,A[i:j]=0
3.计算最优值:
这里有三种方法: 递归、自底向上的动规、自顶向下的动规
(1)递归:
递归方法最直白,也最简单
看代码就可知道,我们从上向下递归,(从A[1,n]开始),一次逐个求解,i==j时,返回0。
对于每一个i,j区间,有一个变量k表示从第k个数组划分,k的范围是[i,j-1],枚举一个区间的所有划分情况然后求最小值就可以了,当然,由于我们是从上向下枚举的,比如划分A[1,4]为A[1:1],A[2,4]的情况,但是这两个子情况我们还不知道,所以就要再次递归,求解每一种子情况的答案~
老师讲的时候说,我们求最小值有两种方法:
(1)设一个INF(“无限大”)让每个求得的值与它比较
(2)先求一个值,让之后每个求得的值与第一个值比较
这里我们用的是第一种方法
#include<iostream>
#include<cstdio>
using namespace std;
const int INF=0x3f3f3f3f;
int p[103],s[103][103],n;
void input(){
printf("请输入矩阵个数:\n");
scanf("%d",&n);
printf("请输入矩阵各维数值:\n");
for(int i=0;i<n+1;i++){
scanf("%d",&p[i]);
}
}
int Recursion(int i,int j){
int u=INF;
if(i==j)return 0;
for(int k=i;k<j;k++){
int t=Recursion(i,k)+Recursion(k+1,j)+p[i-1]*p[k]*p[j];
if(t<u){
u=t;
s[i][j]=k;
}
}
return u;
}
void Traceback(int i,int j){
if(i==j){printf("A%d",i);return ;}
printf("(");
Traceback(i,s[i][j]);
Traceback(s[i][j]+1,j);
printf(")");
}
int main(){
input();
printf("最优解为%d\n",Recursion(1,n));
Traceback(1,n);
printf("\n");
}
递归求解区间问题大概可以看成是一颗递归树,我们发现,有许多重复的计算,避免这些重复计算可以提高效率
(2)自顶向下的动态规划
(动态规划其实也就是避免了重复计算,提高了效率)
自顶向下的方法又叫备忘录,也就是说,在递归的基础上,我们开个数组存值,当需要的值已经求得,直接返回,不用再重复计算了
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
const int INF=0x3f3f3f3f;
int p[103],s[103][103],m[103][103],n;
void input(){
printf("请输入矩阵个数:\n");
scanf("%d",&n);
printf("请输入矩阵各维数值:\n");
for(int i=0;i<n+1;i++){
scanf("%d",&p[i]);
}
}
int MatrixChain_top_bottom(int i,int j){
if(i==j)return 0;
if(m[i][j]!=-1)return m[i][j];
int u=INF;
for(int k=i;k<j;k++){
int t=MatrixChain_top_bottom(i,k)+MatrixChain_top_bottom(k+1,j)+p[i-1]*p[k]*p[j];
if(t<u){
u=t;
s[i][j]=k;
}
m[i][j]=u;
}
return u;
}
void Traceback(int i,int j){
if(i==j){printf("A%d",i);return ;}
printf("(");
Traceback(i,s[i][j]);
Traceback(s[i][j]+1,j);
printf(")");
}
int main(){
input();
memset(m,-1,sizeof(m));
printf("最优解为%d\n",MatrixChain_top_bottom(1,n));
Traceback(1,n);
printf("\n");
}
(3)自底向上的动态规划:
怎么“自底向上”是我们需要考虑的,看了上面的“递归树”我们可能会想,要不从最低一层开始算?这显然是不合理的,因为我们只考虑了问题的物理结构而没有考虑其逻辑结构,即要考虑解决问题方法的可行性。
这时候我们的方法是这样的,求出区间长度1的各段最小值,求出区间长度2的各段最小值……求出区间长度n的各段最小值
思路大概如图:(从底下开始一层一层的看)
代码如下:
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
const int INF=0x3f3f3f3f;
int p[103],s[103][103],m[103][103],n;
void input(){
printf("请输入矩阵个数:\n");
scanf("%d",&n);
printf("请输入矩阵各维数值:\n");
for(int i=0;i<n+1;i++){
scanf("%d",&p[i]);
}
}
void MatrixChain_bottom_to(){
memset(m,0,sizeof(m));
for(int r=2;r<=n;r++){//代表子链长度
for(int i=1;i<=n-r+1;i++){//i从1开始,一直到n-r+1,i表示每一个区间长度为r的区间左端点,i=n-r+1是最后一左个端点,此时区间表示为[n-r+1,n],长度为r
int j=i+r-1;//j表示区间右端点
m[i][j]=m[i][i]+m[i+1][j]+p[i-1]*p[i]*p[j];//这里求最小值用的上述第2中方法,先求一个值,再一次与这个值比较
s[i][j]=i;
for(int k=i+1;k<j;k++){//搜索所有情况,看哪个小于先前的划分
int t=m[i][k]+m[k+1][j]+p[i-1]*p[k]*p[j];
if(t<m[i][j]){m[i][j]=t;s[i][j]=k;}
}
}
}
}
void Traceback(int i,int j){
if(i==j){printf("A%d",i);return ;}
printf("(");
Traceback(i,s[i][j]);
Traceback(s[i][j]+1,j);
printf(")");
}
int main(){
input();
MatrixChain_bottom_to();
printf("最优解为%d\n",m[1][n]);
Traceback(1,n);
printf("\n");
}
4.输出结果:
上面我们可以看到有一个s数组还没有讲到,s数组是为了输出而准备
s[i][j]=k,即区间[i,j]从k出划分是最优的,看代码可以发现,我们每次求得一个更小的情况总要更新s数组,让它记录的值是最优的划分情况:
这里我们可以输出一下s数组的值:
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
printf("%5d",s[i][j]);
}
printf("\n");
}
比如s[1][3]=1表示从对于区间[1,3]从1处划分
s[1][6]=1表示从对于区间[1,6]从1处划分
这时我们要输出划分后的数组是不是容易多了,即用自顶向下的递归方法输出,比如上例输出A[1,6]
看一下,1到6的最优划分在哪呢?s[1][6]=1,从1处划分就行啦啊,这时再分别输出[1:1],[2:6]
[1:1]可以直接输出啦
[2:6]的最优划分在哪呢?s[2][6]=4,这时再分别输出[2:4],[5:6]
[2:4]的最优划分在哪呢?s[2][4]=3,这时再分别输出[2:3],[3:3]
依次类推
注意:我们在每次把大区间化成两个小区间时,前后都要加上括号:
划分[1:6]啦,把A1到A6括起来(A1,A6),
划分[2:6]啦,把A2到A6括起来(A2,A6)
这样就可以得到结果
输出函数:
void Traceback(int i,int j){
if(i==j){printf("A%d",i);return ;}
printf("(");
Traceback(i,s[i][j]);
Traceback(s[i][j]+1,j);
printf(")");
}