代码改变世界

动态规划 矩阵链乘法

  youxin  阅读(1497)  评论(0编辑  收藏  举报

矩阵链乘问题描述

  给定n个矩阵构成的一个链<A1,A2,A3,.......An>,其中i=1,2,...n,矩阵A的维数为pi-1pi,对乘积 A1A2...A以一种最小化标量乘法次数的方式进行加全部括号。

  注意:在矩阵链乘问题中,实际上并没有把矩阵相乘,目的是确定一个具有最小代价的矩阵相乘顺序。找出这样一个结合顺序使得相乘的代价最低。

3、动态规划分析过程

1)最优加全部括号的结构

  动态规划第一步是寻找一个最优的子结构。假设现在要计算AiAi+1....Aj的值,计算Ai...j过程当中肯定会存在某个k值(i<=k<j)将Ai...j分成两部分,使得Ai...j的计算量最小。分成两个子问题Ai...k和Ak+1...j,需要继续递归寻找这两个子问题的最优解。

  有分析可以到最优子结构为:假设AiAi+1....Aj的一个最优加全括号把乘积在Ak和Ak+1之间分开,则Ai..k和Ak+1..j也都是最优加全括号的。

2)一个递归解

  设m[i,j]为计算机矩阵Ai...j所需的标量乘法运算次数的最小值,对此计算A1..n的最小代价就是m[1,n]。现在需要来递归定义m[i,j],分两种情况进行讨论如下:

当i==j时:m[i,j] = 0,(此时只包含一个矩阵)

当i<j 时:从步骤1中需要寻找一个k(i≤k<j)值,使得m[i,j] =min{m[i,k]+m[k+1,j]+pi-1pkpj} (i≤k<j)。

3)计算最优代价

  虽然给出了递归解的过程,但是在实现的时候不采用递归实现,而是借助辅助空间,使用自底向上的表格进行实现。设矩阵Ai的维数为pi-1pi,i=1,2.....n。输入序列为:p=<p0,p1,...pn>,length[p] = n+1。使用m[n][n]保存m[i,j]的代价,s[n][n]保存计算m[i,j]时取得最优代价处k的值,最后可以用s中的记录构造一个最优解。书中给出了计算过程的伪代码。具体代码如下:

复制代码
#include<iostream>
using namespace std;
 
#define maxvalue 1000000

void matrix_chain_order(int p[],int len,int **m,int **s)
{
    int n=len-1;
    //A[i][i]只有一个矩阵,所以相乘次数为0,即m[i][i]=0;
    for(int i=1;i<=n;i++)
    {
        m[i][i]=0;
    }
    for(int L=2;L<=n;L++)//l为chain length
    {
        for(int i=1;i<=n-L+1;i++) ///从第一矩阵开始算起,计算长度为L的最少代价,不是从0
        {
            int j=i+L-1;
            m[i][j]=maxvalue;
            for(int k=i;k<j;k++)
            {
                int 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;
                }
            }
        }
    }
}

void print_optimal_parents(int **s,int i,int j)
{
    if(i==j)
    {
        cout<<"A"<<i;
    }
    else
    {
        cout<<"(";
        print_optimal_parents(s,i,s[i][j]);
        print_optimal_parents(s,s[i][j]+1,j);
        cout<<")";
    }
}


int main()
{
    /*
    现有矩阵A1(30×35)、A2(35×15)、A3(15×5)、A4(5×10)、A5(10×20)、A6(20×25),得到p=<30,35,15,5,10,20,25>
    */
    int p[]={30,35,15,5,10,20,25};
    int len=sizeof(p)/sizeof(int);
    cout<<len<<endl;
    int **m,**s;
    m=new int*[len];
    s=new int*[len];
    for(int i=1;i<len;i++)
    {
        m[i]=new int[len];
        s[i]=new int[len];
    }
     for(int i=1;i<len;i++)
     {
         for(int j=1;j<len;j++)
         {
             m[i][j]=0;
             s[i][j]=0;
         }
     }
     matrix_chain_order(p,len,m,s);
      cout<<"***********M为***********"<<endl;
     for(int i=1;i<len;i++)
     {
         for(int j=1;j<len;j++)
         {
             cout<<m[i][j]<<ends;
         }
         cout<<endl;
     }
     cout<<"***********S为***********"<<endl;
     for(int i=1;i<len;i++)
     {
         for(int j=1;j<len;j++)
         {
             cout<<s[i][j]<<ends;
         }
         cout<<endl;
     }

     print_optimal_parents(s,1,6);

}
复制代码

输出:

 

我们假设矩阵为A1,A2,A3A4A5A6

数组p[7]恰好存储了Ai(维度为p(i-1)*p(i))

上面的代码我们可以按照下面的方式理解:

首先看对角线,矩阵元素长度为1时,m[i][j]=0;因为一个元素没有乘法为0.

长度为2时有2个元素,可以是(1,2),(2,3),(3,4),(4,5),(5,6)

以此类推,

长度为6时只有一种选择了(1,6).

可以看到m[i][j]形成的上三角。

如果采用递归进行实现,则需要指数级时间Ω(2n),因为中间有些重复计算。递归是完全按照第二步得到的递归公式进行计算,递归实现如下所示:

复制代码
int  recursive_matrix_chain(int p[],int len,int **m,int **s,int i ,int j)
{
    if(i==j)
    {
        m[i][j]=0;
    }
    else
    {
        int k;
        m[i][j]=maxvalue;
        for(k=i;k<j;k++)
        {
            int tmp=recursive_matrix_chain(p, len,m,s,i,k)+recursive_matrix_chain(p,len,m,s,k+1,j)+
                    p[i-1]*p[k]*p[j];
            if(tmp<m[i][j])
            {
                m[i][j]=tmp;
                s[i][j]=k;
            }
        }
    }
    return m[i][j];
}
复制代码

只需改下调用方法即可:

// matrix_chain_order(p,len,m,s); 改为

cout<< recursive_matrix_chain(p,len,m,s,1,6)<<endl;

改进递归:

递归重复了很多计算,我们可以做一个备忘录,其思想就是备忘(memorize)原问题的自然但低效的递归算法

加入备忘的递归算法为每一个子问题的解在表中记录一个表项,开始时,美国表现最初都含有一个特殊的值,以表示该值有待填入。当在递归算法的执行中第一次遇到一个子问题时,就计算它的解并填入表中以后。以后每次遇到子问题时,只要查看返回表中先前填入的值即可。

复制代码
int  memoized_matrix_chain(int p[],int len,int **m,int **s)
{
    int n=len-1;
    for(int i=1;i<=n;i++)
        for(int j=i;j<=n;j++)
            m[i][j]=maxvalue;
    return lookup_chain(p,len,m,s,1,n);
}
int lookup_chain(int p[],int len,int **m,int **s,int i,int j)
{
    if(m[i][j]<maxvalue)
    {
        return m[i][j];
    }
    if(i==j)
    {
        m[i][j]=0;
    }
    else
    {
        for(int k=i;k<=j-1;k++)
        {
            int q=lookup_chain(p, len,m,s,i,k)+lookup_chain(p,len,m,s,k+1,j)+
                    p[i-1]*p[k]*p[j];
            if(q<m[i][j])
            {
                m[i][j]=q;
                s[i][j]=k;
            }
        }
    }

    return m[i][j];
}
复制代码

像动态规划的复杂度一样,

memoized_matrix_chain的复杂度也是O(n^3)d .

参考了:http://www.cnblogs.com/Anker/archive/2013/03/10/2952475.html

 http://www.cnblogs.com/weixliu/archive/2012/12/25/2832772.html

编辑推荐:
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
历史上的今天:
2012-08-17 hdu 1003 max sum
2011-08-17 建立WordPress博客网站——个人教程
点击右上角即可分享
微信分享提示