每日算法 — 使用java实现矩阵链乘:区间动态规划最优括号化方案

问题背景

矩阵链乘(Matrix Chain Multiplication)是动态规划的经典入门问题。给定一系列矩阵的维度,求计算它们乘积所需的最少标量乘法次数。矩阵乘法满足结合律,但不同括号化方案的运算代价可能差异巨大。

例如,三个矩阵A(10×30)、B(30×5)、C(5×60)的链乘:
– (AB)C 需要 10×30×5 + 10×5×60 = 4500 次乘法
– A(BC) 需要 30×5×60 + 10×30×60 = 27000 次乘法

最优方案的计算量仅为次优方案的六分之一,在大型矩阵运算中这一差距会被急剧放大。

核心算法思想

区间DP状态设计

设 dp[i][j] 为计算矩阵 A_i × A_{i+1} × ... × A_j 所需的最少乘法次数。状态转移方程为:

dp[i][j] = min(dp[i][k] + dp[k+1][j] + p[i-1]*p[k]*p[j])  (i ≤ k < j)

其中 p[i-1]×p[k] 是左半部分结果的维度,p[k]×p[j] 是右半部分结果的维度,合并需要 p[i-1]*p[k]*p[j] 次乘法。

递推顺序

按区间长度从小到大枚举,确保计算 dp[i][j] 时所有子区间 dp[i][k] 和 dp[k+1][j] 均已计算完毕。

Java完整实现

import java.util.*;

/**
 * 矩阵链乘问题求解器
 * 使用区间动态规划求最优括号化方案
 */
public class MatrixChainMultiplication {

    private int[] dims;           // 矩阵维度数组,dims[i-1]×dims[i]为第i个矩阵
    private int n;                // 矩阵个数
    private int[][] dp;           // dp[i][j]表示第i到第j个矩阵的最小代价
    private int[][] split;        // split[i][j]记录最优分割点k

    public MatrixChainMultiplication(int[] dims) {
        this.dims = dims;
        this.n = dims.length - 1; // n个矩阵需要n+1个维度值
        this.dp = new int[n + 1][n + 1];
        this.split = new int[n + 1][n + 1];
    }

    /**
     * 求解最优矩阵链乘方案
     * @return 最小标量乘法次数
     */
    public int solve() {
        // 初始化:单个矩阵乘法代价为0
        for (int i = 1; i <= n; i++) {
            dp[i][i] = 0;
        }

        // 按区间长度从小到大枚举
        for (int len = 2; len <= n; len++) {
            for (int i = 1; i <= n - len + 1; i++) {
                int j = i + len - 1;
                dp[i][j] = Integer.MAX_VALUE;

                // 枚举分割点k:在k和k+1之间分割
                for (int k = i; k < j; k++) {
                    // 左半部分代价 + 右半部分代价 + 合并代价
                    int cost = dp[i][k] + dp[k + 1][j]
                            + dims[i - 1] * dims[k] * dims[j];
                    if (cost < dp[i][j]) {
                        dp[i][j] = cost;
                        split[i][j] = k;
                    }
                }
            }
        }

        return dp[1][n];
    }

    /**
     * 递归构造最优括号化方案
     */
    public String getOptimalParenthesization(int i, int j) {
        if (i == j) {
            return "A" + i;
        }
        int k = split[i][j];
        String left = getOptimalParenthesization(i, k);
        String right = getOptimalParenthesization(k + 1, j);
        return "(" + left + " × " + right + ")";
    }

    /**
     * 打印DP表用于调试
     */
    public void printDPTable() {
        System.out.println("DP表(最小乘法次数):");
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= n; j++) {
                if (j < i) {
                    System.out.printf("%8s ", "-");
                } else {
                    System.out.printf("%8d ", dp[i][j]);
                }
            }
            System.out.println();
        }
    }

    /**
     * 迭代版本的矩阵链乘(基于自底向上DP)
     * 与solve()逻辑一致,显式展示递推过程
     */
    public int solveIterative() {
        // dp[i][j]已自动初始化为0
        for (int len = 2; len <= n; len++) {
            for (int i = 1; i <= n - len + 1; i++) {
                int j = i + len - 1;
                dp[i][j] = Integer.MAX_VALUE;
                for (int k = i; k < j; k++) {
                    int q = dp[i][k] + dp[k + 1][j]
                            + dims[i - 1] * dims[k] * dims[j];
                    if (q < dp[i][j]) {
                        dp[i][j] = q;
                        split[i][j] = k;
                    }
                }
            }
        }
        return dp[1][n];
    }

    public static void main(String[] args) {
        // 示例:6个矩阵,维度分别为 30×35, 35×15, 15×5, 5×10, 10×20, 20×25
        int[] dims = {30, 35, 15, 5, 10, 20, 25};
        MatrixChainMultiplication mcm = new MatrixChainMultiplication(dims);

        int minCost = mcm.solve();
        System.out.println("最小标量乘法次数: " + minCost);
        System.out.println("最优括号化方案: " + mcm.getOptimalParenthesization(1, 6));
        mcm.printDPTable();

        // 对比示例:A(10×30), B(30×5), C(5×60)
        int[] dims2 = {10, 30, 5, 60};
        MatrixChainMultiplication mcm2 = new MatrixChainMultiplication(dims2);
        int minCost2 = mcm2.solve();
        System.out.println("\n示例2 最小乘法次数: " + minCost2);
        System.out.println("示例2 最优方案: " + mcm2.getOptimalParenthesization(1, 3));
        // (AB)C = 10*30*5 + 10*5*60 = 1500 + 3000 = 4500
        // A(BC) = 30*5*60 + 10*30*60 = 9000 + 18000 = 27000
    }
}

算法复杂度分析

  • 状态数量:O(n²),共有 n(n+1)/2 个区间状态
  • 状态转移:每个状态枚举分割点 k,O(n)
  • 总时间复杂度:O(n³)
  • 空间复杂度:O(n²),用于存储dp表和分割点表

对于大规模矩阵链(n > 200),O(n³) 的复杂度可能较高。此时可考虑:
– Knuth优化:若满足四边形不等式,可将复杂度降至 O(n²)
– 并行计算:不同区间长度的计算可部分并行化

算法扩展与变种

最优二叉搜索树

矩阵链乘与最优二叉搜索树(OBST)的DP结构高度相似,均采用区间DP框架。OBST的期望搜索代价最小化可以看作带权版本的矩阵链乘问题。

多边形三角剖分

凸多边形的最优三角剖分同样可转化为区间DP,与矩阵链乘同构。每个三角剖分方案对应一种矩阵链乘的括号化方案。

记忆化搜索实现

区间DP也可采用自顶向下的记忆化搜索实现,代码更直观但常数稍大:

public int memoSolve(int i, int j) {
    if (i == j) return 0;
    if (dp[i][j] != -1) return dp[i][j];
    dp[i][j] = Integer.MAX_VALUE;
    for (int k = i; k < j; k++) {
        int cost = memoSolve(i, k) + memoSolve(k + 1, j)
                 + dims[i-1] * dims[k] * dims[j];
        dp[i][j] = Math.min(dp[i][j], cost);
    }
    return dp[i][j];
}

区间DP的核心在于”大事化小”:将大区间最优解分解为小区间最优解的组合。掌握这一思想后,大量看似复杂的问题都能被优雅地解决。