动态规划 矩阵相乘问题


原文链接:https://www.cnblogs.com/fsmly/p/10228767.html


description

动态规划实现矩阵链乘法问题

矩阵链乘法问题( matrix-chain multiplication problem  )

  (1)问题描述

  给定n个矩阵的链<A 1 ,A 2 ,…,A n >,其中i=1,2,…,n,矩阵A i的维数为p i-1 ×p i 。求一个完全“括号化方案”,使得计算乘积A 1 A 2 …A n 所需的标量乘法次数最小
  (2)最优括号化方案的结构特征

  用记号 A i,j 表示 A i A i+1 …A j 通过加括号后得到的一个最优计算模式,且恰好在A k 与A k+1 之间分开。则“前缀”子链A i A i+1 …A k 必是一个最优的括号化子方案,记为A i,k ;同理“后缀”子链A k+1 A k+2 …A j 也必是一个最优的括号化子方案,记为A k+1,j。

  (3)一个递归求解的方案

  对于矩阵链乘法问题,我们将所有对于1≤i≤j≤n确定A i A i+1 …A j 的最小代价括号方案作为子问题。令m[i,j]表示计算矩阵A i,j 所需要的标量乘法的次数最小值,则最优解就是计算A i...n所需的最低代价就是m[1,n] 

  递归定义m[i,j]。

  ①对于i=j的情况下,显然有m=0,不需要做任何标量乘法运算。所以,对于所有的i=1、2......n,m[i,i] = 0.

  ②当i < j的情况,就按照最优括号化方案的结构特征进行计算m[i,j]。假设最优括号化方案的分割点在矩阵Ak和Ak+1之间,那么m的值就是Ai...k和Ak+1...j的代价加上两者量程的代价的最小值,即:

在这里插入图片描述

该公式的假设是最优分割点是已知的,但是实际上不知道。然而,k只有j-i中情况取值。由于最优分割点k必定在i~j内取得,只需要检查所有可能的情况,找到最优解即可。可以得出一个递归公式

在这里插入图片描述

  m只是给出了子问题最优解的代价,但是并未给出构造最优解的足够信息(即分割点的位置信息)。所以,在此基础之上,我们使用一个二维数组s[i,j]来保存 A i A i+1 …A 的分割点位置k。

  (4)我们采用自底向上表格方法来代替上述递归公式算法来计算最优代价。过程中假定矩阵A的规模为Pi-1Xpi,输入是一个序列p=<p0,p1,......,pn>,长度为p.length = n+1.其中使用一个辅助表m来记录代价m[i,j],另一个表s来记录分割点的位置信息,以便于构造出最优解。

在这里插入图片描述

  简单介绍一下算法:首先在3~4行对所有的i=1、2......n计算m[i,i]=0。然后在5~13行中的第一个for循环,使用(3)中的递归公式对所有的i=1~n-1计算m[i,i+1](长度为l=2的最小计算代价)的值。在第二个循环中,对所有的i=i~n-2计算m[i,i+2](长度为l=3的链的最小计算代价)的值。到最后,10~13行中计算代价m[i,j]的时候仅仅依赖于上面计算的表项m[i,k]和m[k+1,j]

  (5)给一个简单的例子 

  ①给出一个n=6的矩阵如下图所示

在这里插入图片描述

  ②由上述表我们按照下面这样的计算方式来得到m[i,j]所对应的表,下图所示的表格中代表的m[i,j]的最小值

 

在这里插入图片描述

  ③可以得到下面这样一张表

  首先将矩阵化为一张一维数组表

在这里插入图片描述

  ④简单结算其中的一些表项,给出一个m[1,3]的值的计算过程如下:

在这里插入图片描述

  可以得出上面的比较小的分割点为1,所以s[1,3] = 1。

在这里插入图片描述

  可以得出分割点的位置为s[2,5] = 3。

  上面给出了一个简单的两个点的计算。下图是计算完成的矩阵m和s

在这里插入图片描述
在这里插入图片描述

   由上面表s可以得到,最优解为(A1(A2A3))((A4A5)A6)

 

code

<1> 自底向上

  1. 转载的文章里面没有写代码,代码是我根据《算法导论》中的伪代码写的
/******************************************
 * @Author       : 鱼香肉丝没有鱼
 * @Date         : 2021-09-20 12:55:54
 * @LastEditors  : 鱼香肉丝没有鱼
 * @LastEditTime : 2021-11-24 01:29:41
 ******************************************/

// 矩阵连乘
#include <iomanip>
#include <iostream>
#include <limits>
#include <stdio.h>
#include <stdlib.h>

using namespace std;

const int N = 100;
int P[N];
int M[N][N];
int S[N][N];

void OutputM(int n)
{
    for(int i = 1; i < n + 1; i++) {
        for(int j = 1; j < n + 1; j++) {
            cout << setw(5) << M[i][j] << " ";
        }
        cout << endl;
    }
    cout << endl;
}

// s[i][j]中的数表明,
// 计算矩阵链A[i:j]的最佳方式应在矩阵Ak和Ak+1之间断开,
// 即最优的加括号方式应为(A[i:k])(A[k+1:j)。
void OutputS(int n)
{
    for(int i = 1; i < n + 1; i++) {
        for(int j = 1; j < n + 1; j++) {
            cout << setw(2) << S[i][j] << " ";
        }
        cout << endl;
    }
    cout << endl;
}

// 我们只需要使用上三角就行了
void MatrixChainOrder(int n)
{
    int i, j, k, r;
    for(i = 1; i <= n; i++)
        M[i][i] = 0;  //初始化对角线,都是0

    // OutputM(n);
    // OutputS(n);

    for(r = 2; r <= n; r++) {  // r个矩阵相乘,从两个一直到n个,遍历所有组合情形
        for(i = 1; i <= n - r + 1; i++) {  //在r-1个空隙中依次测试最优点 ,i的右边界保证最后剩下r个矩阵
            j = i + r - 1;  //他自己也要算是r个矩阵中的一个,所以要减1,也就是从第i个到第j个刚好有r个矩阵
            M[i][j] = INT_MAX;  //先初始化为很大,待会对更新不造成影响
            // OutputM(n);
            for(k = i; k <= j - 1; k++) {
                int tmp = M[i][k] + M[k + 1][j] + P[i - 1] * P[k] * P[j];
                if(tmp < M[i][j]) {
                    M[i][j] = tmp;  //更新使用更小的值
                    S[i][j] = k;  //括号加的位置
                }
            }
            // OutputM(n);
            // OutputS(n);
        }
    }
}

void Traceback(int i, int j)
{
    if(i == j) {
        cout << "P[" << i << "]";

        return;
    }
    cout << "(";
    Traceback(i, S[i][j]);
    Traceback(S[i][j] + 1, j);
    cout << ")";
}

int main()
{
    freopen("file out.txt", "r", stdin);
    int n;  //矩阵个数
    cin >> n;

    for(register int i = 0; i <= n; i++)  // arr的0号位也要使用
        cin >> P[i];

    MatrixChainOrder(n);

    cout << "添括号的方式最好为:" << endl;
    Traceback(1, n);
    cout << endl;

    cout << "最小的计算量为:" << M[1][n] << endl;

    return 0;
}

// 6
// 30 35 15 5 10 20 25

输出:

添括号的方式最好为:
((P[1](P[2]P[3]))((P[4]P[5])P[6]))
最小的计算量为:15125

<2> 递归

  1. 代码来源 《计算机算法与分析》
/******************************************
 * @Author       : 鱼香肉丝没有鱼
 * @Date         : 2021-09-20 12:55:54
 * @LastEditors  : 鱼香肉丝没有鱼
 * @LastEditTime : 2021-11-24 01:29:41
 ******************************************/

// 矩阵连乘
#include <iomanip>
#include <iostream>
#include <limits>
#include <stdio.h>
#include <stdlib.h>

using namespace std;

const int N = 100;
int P[N];
int S[N][N];

// s[i][j]中的数表明,
// 计算矩阵链A[i:j]的最佳方式应在矩阵Ak和Ak+1之间断开,
// 即最优的加括号方式应为(A[i:k])(A[k+1:j)。
void OutputS(int n)
{
    for(int i = 1; i < n + 1; i++) {
        for(int j = 1; j < n + 1; j++) {
            cout << setw(2) << S[i][j] << " ";
        }
        cout << endl;
    }
    cout << endl;
}

void Traceback(int i, int j)
{
    if(i == j) {
        cout << "P[" << i << "]";

        return;
    }
    cout << "(";
    Traceback(i, S[i][j]);
    Traceback(S[i][j] + 1, j);
    cout << ")";
}

// 直接计算A[i:j]
int RecurMatrixChain(int i, int j)
{
    if(i == j)
        return 0;
    int u = RecurMatrixChain(i, i) + RecurMatrixChain(i + 1, j) + P[i - 1] * P[i] * P[j];
    S[i][j] = i;
    for(int k = i + 1; k < j; k++) {
        int t = RecurMatrixChain(i, k) + RecurMatrixChain(k + 1, j) + P[i - 1] * P[k] * P[j];
        if(t < u) {
            u = t;
            S[i][j] = k;
        }
    }
    return u;
}

int main()
{
    freopen("file out.txt", "r", stdin);
    int n;  //矩阵个数
    int count=0;//计算量
    cin >> n;

    for(register int i = 0; i <= n; i++)  // arr的0号位也要使用
        cin >> P[i];

    count = RecurMatrixChain(1, 6);

    cout << "添括号的方式最好为:" << endl;
    Traceback(1, n);
    cout << endl;

    cout << "最小的计算量为:" << count << endl;

    return 0;
}

// 6
// 30 35 15 5 10 20 25

<3>备忘录方法

  1. 自顶向下的方法,来源 《计算机算法与设计》
  2. 代码;
/******************************************
 * @Author       : 鱼香肉丝没有鱼
 * @Date         : 2021-09-20 12:55:54
 * @LastEditors  : 鱼香肉丝没有鱼
 * @LastEditTime : 2021-11-24 01:29:41
 ******************************************/

// 矩阵连乘
#include <iomanip>
#include <iostream>
#include <limits>
#include <stdio.h>
#include <stdlib.h>

using namespace std;

const int N = 100;
int P[N];
int M[N][N];
int S[N][N];

void OutputM(int n)
{
    for(int i = 1; i < n + 1; i++) {
        for(int j = 1; j < n + 1; j++) {
            cout << setw(5) << M[i][j] << " ";
        }
        cout << endl;
    }
    cout << endl;
}

// s[i][j]中的数表明,
// 计算矩阵链A[i:j]的最佳方式应在矩阵Ak和Ak+1之间断开,
// 即最优的加括号方式应为(A[i:k])(A[k+1:j)。
void OutputS(int n)
{
    for(int i = 1; i < n + 1; i++) {
        for(int j = 1; j < n + 1; j++) {
            cout << setw(2) << S[i][j] << " ";
        }
        cout << endl;
    }
    cout << endl;
}

//输出最好的加括号方式
void Traceback(int i, int j)
{
    if(i == j) {
        cout << "P[" << i << "]";

        return;
    }
    cout << "(";
    Traceback(i, S[i][j]);
    Traceback(S[i][j] + 1, j);
    cout << ")";
}

int LookupChain(int i, int j)
{
    if(M[i][j] > 0)
        return M[i][j];  //已经被计算过,直接使用就行了
    if(i == j)
        return 0;
    int u = LookupChain(i, i) + LookupChain(i + 1, j) + P[i - 1] * P[i] * P[j];
    S[i][j] = i;
    for(int k = i + 1; k < j; k++) {
        int t = LookupChain(i, k) + LookupChain(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;
}

int MemoizeMatrixChain(int n)
{
    for(int i = 1; i <= n; i++) {
        for(int j = 1; j <= n; j++) {
            M[i][j] = 0;  //初始化为0,表示还没有计算过
        }
    }
    return LookupChain(1, n);
}

int main()
{
    freopen("file out.txt", "r", stdin);
    int n;  //矩阵个数
    cin >> n;

    for(register int i = 0; i <= n; i++)  // arr的0号位也要使用
        cin >> P[i];

    MemoizeMatrixChain(n);

    cout << "添括号的方式最好为:" << endl;
    Traceback(1, n);
    cout << endl;

    cout << "最小的计算量为:" << M[1][n] << endl;

    return 0;
}

// 6
// 30 35 15 5 10 20 25
  • 2
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值