动态规划---矩阵链连乘

问题描述

给定若干个矩阵,寻找最优的相乘次序,使得乘法运算的次数最少,并输出对应的最少运算次数。比如现有三个矩阵ABC,维数分别为\(A:2\times10\)\(B:10\times2\)\(C:2\times10\) 。虽然(AB)C=A(BC) 结果是相等的,即与相乘次序没有关系。但是(AB)C乘法运算的次数为\(2\times10\times2+2\times2\times10=80\),而A(BC)为\(10\times2\times10+2\times10\times10=400\),显然(AB)C的运算次数更少,即效率更高。

思路

求解的关键在于如何将问题分解为若干子问题。我们想象在各个矩阵之间可以放上隔板,那么只要先分别求解左和右的最少乘法次数,再将隔板左右的两部分相乘,就可以得到当前分隔方法的最优解,最后通过比较各种分隔方法,就能得到当前长度的最优解,如下图中四种分隔中我们取最少的次数,即为长度5(5个连续矩阵)的最优解。由于分隔后的长度必定小于当前长度,如此处的子问题长度必定小于5,而子问题已经在之前的迭代过程求得,无需重复计算。通过下一部分的迭代过程展示能有更直观的理解。

迭代过程

给定5个矩阵: ABCDE ,维数分别为\(A:30\times35\) , \(B:35\times15\), \(C:15\times5\) , \(D:5\times10\), \(E:10\times20\) 。从矩阵链长度为1开始求解,然后求解长度为2的,最终到达5。dp[]数组(详见算法实现)的更新如下图所示。

为了更直观的理解,下图和上面是等效的。(符号说明:m[a,b]代表从序号为a到序号为b的矩阵链所需的最少乘法次数,特别地,m[a,a]代表a号矩阵本身,很明显m[a,a]=0。)

算法实现

arr[]数组用于记录矩阵链信息,其中n号矩阵对应的维数是arr[n-1]*arr[n]。动态规划的核心算法利用3个for循环,最外层控制矩阵链长度,下一层控制起始点,再下一层控制隔板的位置。最后左右合并的时候注意下标的选择,如下图所示。

#include <iostream>
#include <limits>
#include <cstring>
using namespace std;
int dp[51][51]; //从1开始,不使用0

// output: Minimum number of multiplications is 11875


/*功能:寻找矩阵链最优的运算次数
返回值:相对应的运算次数 */
int MinMatrixMul(int arr[], int num){
    int start, mid, end;
    int times; //记录当前的最优解,即最少的运算次数

    //dp[i][i]=0 已经在初始化的时候完成
    //dp[i][j] 代表i号到j号矩阵链的运算次数
    for (int length = 2; length < num; ++length) {

        for (start = 1; start <= num-length; ++start) {
            end = start + length - 1;
            dp[start][end] = INT_MAX;

            for (mid = start; mid < end; ++mid) {
                times = dp[start][mid] + dp[mid+1][end] + arr[start-1]*arr[mid]*arr[end];

                //如果找到当前长度下更优的解,立刻更新dp数组
                if (times < dp[start][end]) {
                    dp[start][end] = times;
                }
            }
        }
    }
    return dp[1][num-1];
}

int main() {
    int num;
    int arr[] = {30, 35, 15, 5, 10, 20};
    memset(dp, 0, sizeof(dp));
    num = sizeof(arr)/ sizeof(int);

    cout << "Minimum number of multiplications is "<< MinMatrixMul(arr, num) << endl;

}

拓展:尝试写出计算顺序,即打印括号

思路:增加bracket数组,bracket[start][end]记录从start到end当中的mid,即加括号的地方。若start==end说明只有一个矩阵,直接输出。令name(使用引用)从'A'开始,一旦输出一个,便执行++name。

#include <iostream>
#include <limits>
#include <cstring>
using namespace std;
int dp[51][51]; //从1开始,不使用0
int bracket[51][51]; //记录括号添加的位置

/*output:
 * Minimum number of multiplications is 11875
 * Order: ((A(BC))(DE))
 * */

/*功能:打印括号
 *返回值:无 */
void printBrackets(int start, int end, char &name){
    if (start == end) {
        cout << name;
        ++name;
        return;
    }
    cout << "(";
    printBrackets(start, bracket[start][end], name);
    printBrackets(bracket[start][end]+1, end, name);
    cout << ")";
}


/*功能:寻找矩阵链最优的运算次数
 *返回值:相对应的运算次数 */
int MinMatrixMul(int arr[], int num){
    int start, mid, end;
    int times; //记录当前的最优解,即最少的运算次数

    //dp[i][i]=0 已经在初始化的时候完成
    //dp[i][j] 代表i号到j号矩阵链的运算次数
    for (int length = 2; length < num; ++length) {

        for (start = 1; start <= num-length; ++start) {
            end = start + length - 1;
            dp[start][end] = INT_MAX;

            for (mid = start; mid < end; ++mid) {
                times = dp[start][mid] + dp[mid+1][end] + arr[start-1]*arr[mid]*arr[end];

                //如果找到当前长度下更优的解,立刻更新dp数组
                if (times < dp[start][end]) {
                    dp[start][end] = times;

                    //更新添加括号的位置
                    bracket[start][end] = mid;
                }
            }
        }
    }
    return dp[1][num-1];
}

int main() {
    int num;
    char name = 'A';
    int arr[] = {30, 35, 15, 5, 10, 20};
    memset(dp, 0, sizeof(dp));
    num = sizeof(arr)/ sizeof(int);

    cout << "Minimum number of multiplications is "<< MinMatrixMul(arr, num) << endl;
    cout << "Order: " ;
    printBrackets(1, num-1, name);

}

参考资料

http://www.geeksforgeeks.org/dynamic-programming-set-8-matrix-chain-multiplication/

http://www.geeksforgeeks.org/printing-brackets-matrix-chain-multiplication-problem/

posted @ 2017-05-18 20:37  ChaseChoi  阅读(705)  评论(0编辑  收藏  举报