3.2矩阵连乘问题 - 递归、自底向上的动规、自顶向下的动规

前提:

输入矩阵的个数,和各个维度,保证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(")");
}

阅读更多
想对作者说点什么?

博主推荐

换一批

没有更多推荐了,返回首页