dp-矩阵链相乘顺序

矩阵链相乘顺序

问题描述

A1,A2,..,An 表示n个矩阵的序列,其中Ai为\(P_{i−1}×P_i\)阶矩阵,i=1,2,...,n。
向量P=<P0,P1,P2..Pi>表示矩阵链的输入,其中P0是A1的行数,P1是A1的列数,P1是A2的行数,以此类推。
计算这个矩阵需要做n−1次两个矩阵的相乘运算,可以用n−1对括号表示运算次序。
因为矩阵乘法满足结合律,所以无论采用那种顺序,最后结果都一样,但是采用不同的顺序计算的工作量不同。如何定义两个矩阵相乘的工作量呢?
所以假设A1有i行k列,A2有k行j列。所以A1A2相乘后的矩阵有i行j列,含ij个元素。
以元素相乘作为基本运算,乘积中每个元素的计算都需要做j次乘法,于是计算A1A2总共需要ijk次乘法。

举例说明

假设输入的是P=<10,100,5,50>,说明有3个矩阵相乘。其中,
A1:10×100
A2:100×50
A3:5×50

有两种乘法次序:
(A1A2)A3
A1(A2A3)

执行第一种运算的基本运算次序:10×100×5+10×5×50=7500
执行第二种运算的基本运算次序:10×100×50+100×5×50=75000
工作量相差达10倍!
所以我们的问题是:给定向量P,确定一种乘法次序,使得基本运算的总次数最少。

问题分析

1、寻找最优子结构:
假设我们已经找到父矩阵链最优解,当我们划分到最后一步时都是两个子矩阵链(分别被括号包围)相乘,如(A1A2A3A4)(A5A6A7),此时显然外括号为父矩阵的最优括号划分。继续往下划分,((A1A2A3)A4)(A5(A6A7)),则两个子矩阵链的划分括号必须也为他们本身的最优括号划分。因为如果子矩阵链的划分括号不是他们本身的最优括号划分时,两个子矩阵链就有另一种最优括号划分,如(A1(A2A3A4))(A5(A6A7))。那么就与我们的假设相悖。((A1A2A3)A4)(A5(A6A7))不是我们父矩阵最优解。所以拥有最优划分的父矩阵的子矩阵链显然也拥有最优划分。

2、子问题重叠:
在分解子结构的过程中,会多次访问同一个子问题,比如 (A1(A2A3A4))(A5(A6A7)) 和 (A1(A2A3A4)A5)(A6A7)。
这正是动态规划解决的问题。

程序

#include <iostream>
#include <vector>
#include <new>
#include <climits>

using namespace std;

int solve(const vector<pair<int, int>>& mat_chain);
void print_mat(int** mat, int len1, int len2);


int main (int argc, char** argv) {
    vector<pair<int, int>> mat_chain{
        {10,100},
        {100,5},
        {5,50},
        {50,9},
    };

    int ret = solve(mat_chain);
    cout << "ret: " << ret << endl;

    return 0;
}

int solve(const vector<pair<int, int>>& mat_chain) {
    int len = mat_chain.size();
    int i, j, k, diff, sum;
    int** order = new int*[len];
    // mount 右上存放计算量:[i,j]矩阵链的最小计算量为mount[i,j]
    // mount 左下存放计算顺序:mount[i][j]=k 表示[i,j]矩阵链分割为[i,k]和[k+1,j]两部分
    //   先分别计算这两部分,再对其相乘
    int** mount = new int*[len];
    for (i = 0; i < len; ++i) {
        mount[i] = new int[len];
        for (j = 0; j < len; ++j) {
            mount[i][j] = INT_MAX;
        }
        // 对角线表示自己和自己相乘,计算量为0
        mount[i][i] = 0;
    }

    for (diff = 1; diff < len; ++diff) {
        for (i = 0; i < len - diff; ++i) {
            j = i + diff;
            for (k = i; k < j; ++k) {
                // 计算量=分割的两部分各自的计算量加上两部分相乘的计算量
                sum = mount[i][k] + mount[k+1][j]
                    + mat_chain[i].first * mat_chain[k].second * mat_chain[j].second;
                if (sum < mount[i][j]) {
                    mount[i][j] = sum;
                    mount[j][i] = k;
                }
            }
        }
    }
    cout << "mount:\n";
    print_mat(mount, len, len);
    sum = mount[0][len-1];

    for (i = 0; i < len; ++i) {
        delete[] mount[i];
    }
    delete[] mount;

    return sum;
}

void print_mat(int** mat, int len1, int len2) {
    for (int i = 0; i < len1; ++i) {
        for (int j = 0; j < len2; ++j) {
            cout << mat[i][j] << "\t";
        }
        cout << endl;
    }
}

测试:

mount:
0	5000	7500	7700
0	0	25000	6750
1	1	0	2250
1	1	2	0
ret: 7700
posted @ 2023-08-13 16:35  keep-minding  阅读(36)  评论(0编辑  收藏  举报