給定 n 個矩陣 A₁, A₂, ..., Aₙ,其中 Aᵢ 的維度為 p_{i-1} × p_i。矩陣乘法滿足結合律但不滿足交換律,不同加括號順序導致標量乘法次數不同。目標:找到完全括號化方案,使總標量乘法次數最小,並輸出最優方案與結果矩陣。
對於區間 [i, j] (i ≤ j) 的最優括號化,必存在分割點 k (i ≤ k < j),使左半部 [i, k] 和右半部 [k+1, j] 均為最優,且總代價 = 左代價 + 右代價 + 合併代價 p_{i-1} * p_k * p_j。這體現了最優子結構與重疊子問題。
設 m[i][j] 為計算 A_i ... A_j 的最小代價,s[i][j] 為對應的最優分割點。則:
m[i][j] = 0, if i == j
m[i][j] = min_{i≤k
按鏈長 l = 2 到 n 遞增,對每個起點 i 計算 j = i+l-1,枚舉 k 更新 m[i][j] 和 s[i][j]。時間 O(n³),空間 O(n²)。
利用 s[i][j] 遞歸打印括號化方案;利用分割點遞歸實際相乘矩陣 (需實現矩陣乘法類)。
// 矩陣類:支持乘法、輸出、隨機初始化
class Matrix {
int row, col;
double** val;
public:
Matrix(int r=0, int c=0) : row(r), col(c) { alloc(); }
Matrix operator*(const Matrix& o) const { ... }
friend ostream& operator<<(ostream&, const Matrix&);
Matrix init(); // 隨機填充
};
// 矩陣鏈乘 DP
void matrix_chain_order(const vector& p, vector>& m, vector>& s) {
int n = p.size() - 1; // n 個矩陣
m.assign(n+1, vector(n+1, 0));
s.assign(n+1, vector(n+1, 0));
for (int l = 2; l <= n; ++l) {
for (int i = 1; i <= n - l + 1; ++i) {
int j = i + l - 1;
m[i][j] = LLONG_MAX;
for (int k = i; k < j; ++k) {
long long q = m[i][k] + m[k+1][j] + 1LL * p[i-1] * p[k] * p[j];
if (q < m[i][j]) {
m[i][j] = q;
s[i][j] = k;
}
}
}
}
}
// 打印括號化方案
void print_optimal_parens(const vector>& s, int i, int j) {
if (i == j) cout << "A" << i;
else {
cout << "(";
print_optimal_parens(s, i, s[i][j]);
print_optimal_parens(s, s[i][j]+1, j);
cout << ")";
}
}
// 實際相乘得到結果矩陣
Matrix multiply_chain(const vector>& s, int i, int j, const vector& mats) {
if (i == j) return mats[i-1];
Matrix left = multiply_chain(s, i, s[i][j], mats);
Matrix right = multiply_chain(s, s[i][j]+1, j, mats);
return left * right;
}
矩陣鏈乘是動態規劃的教科書級範例,完美展示了「刻畫最優結構 → 遞歸定義 → 自底向上填表 → 回溯構造解」的標準流程。掌握此題有助於理解區間 DP、最優二叉搜索樹、多邊形三角剖分等同類問題。