動態規劃 (二) 最優矩陣鏈乘
阿新 • • 發佈:2019-02-16
背景分析
最優矩陣鏈乘是二維的動態規劃問題,也是較為經典的動態規劃入門問題。《演算法導論》和劉汝佳的《演算法競賽入門經典》中都有詳細描述。問題描述
線上性代數裡,我們都學過矩陣的乘法。矩陣的乘法不滿足分配律,但是滿足結合律,因此 (A x B)x C 與 A x(B x C)的運算結果是一樣的,但是中間的運算量卻可能不一樣。 例如:假設A、B、C矩陣分別是 2 x 3、3 x 4、4 x 5 的,則 (A x B)x C 的運算量是 2 x 3 x 4 + 2 x 4 x 5 = 64,但是 A x(B x C)的運算量是 3 x 4 x 5 + 2 x 3 x 5 = 90。(想想運算量為什麼是這麼計算的。)顯然第一種計算方法比較節省計算量。 好,現在考慮下面的問題:給出n個矩陣組成的序列,設計一種方法,把他們按照一定的順序依次乘起來(你也可以理解為不斷地加括號),是的總的計算量儘量小。假設第i個矩陣 Ai思路分析
我們先把這n個矩陣 A1 X A2 X A3 X ...... X An分成兩份,P = A1 X A2 X ... X Ak和 Q = Ak+1 X Ak+2 X ... X An,其中 1<= k < n。顯然所求的結果可以由PXQ得到,且答案是k從1到n-1迴圈一邊,運算結果最小的那個。這是一個多階段決策的最優化問題,我們可以用動態規劃解決。現在我們用dp[i][j]表示矩陣Ai X Ai+1 X ... X Aj(顯然 i < j,否則無意義)鏈乘的最少運算次數,那麼可以得到下面的狀態轉移方程:f(i, j) = min { f(i, k) + f(k+1, j) + P(i-1)P(k)P(j) }, i <= k < j
兩個相鄰的狀態,可以通過一次矩陣鏈乘得到。若還是不理解這個方程,可以把 k 從 i 到 j-1 列舉出來,相應的加上括號做乘法,再選一個最大值。
演算法實現
我們可以先用好理解的記憶化搜尋來實現(廢話比較多,可以直接只看search函式)但是如果要寫成遞推式,就要考慮一下迴圈的順序了。遞推和記憶化搜尋的區別就在這兒,記憶化搜尋不用擔心迴圈的順序,但是遞推只能呼叫前面已經算出來過的式子。書上一般給出的解決方法時,從長度短的遞推到長度長的。就是說,先把所有兩個矩陣鏈乘的情況都算出來,再算三個矩陣鏈乘的情況,那麼此時算兩個矩陣的情況就是三個的最優子問題了。const int INF_NUM = 0x3f3f3f3f; const int MAX = 1002; int dp[MAX][MAX]; // dp[i][j]表示第i個矩陣鏈乘到第j個的最少運算次數 int p[MAX]; // 矩陣維數 int matrix_chain_memorized() { int n = strlen(p) - 1; // n為矩陣的個數,即p陣列長度減一 read_data(n); // 讀入資料,此處略去 memset(dp, 0x3f, sizeof(dp)); return search(1, n); } int search(int i, int j) { // 記憶化 if(dp[i][j] < INF_NUM) return dp[i][j]; // 初始化條件 if(i == j) dp[i][j] = 0; // tag else { // 迴圈一遍,找到最小的 for(int k = i; k < j; ++ k) { int ans = search(i, k) + search(k+1) + p[i-1]*p[k]*p[j]; if(ans < dp[i][j]) dp[i][j] = ans; } } return dp[i][j]; }
我們可以在外面迴圈設定一個變數 len 來表示此時矩陣鏈乘的長度,當然 j 的值也就固定位 i + len - 1了。
程式碼如下:
void matrix_chain()
{
// 初始化,即長度為 1 的情況
for(i = 0; i < n; ++ i)
dp[i][i] = 0;
// 從長度為 2 開始迴圈
for(len = 2; len <= n; ++ len)
{
for(i = 1; i <= n - len + 1; ++ i)
{
j = i + len - 1;
for(k = i; k < j; ++ k)
{
int ans = dp[i][k] + dp[k+1][j] + p[i-1] * p[k] * p[j];
if(dp[i][j] > ans)
dp[i][j] = ans;
}
}
}
cout << dp[1][n] << endl;
}
我們來嘗試走一次迴圈,以便理解為什麼 for 迴圈的邊界要這樣子設定 首先是長度 len = 2,表示這一遍迴圈會計算所有Ai X Ai+1的情況,即兩個連續的矩陣相乘的情況。i 會 從 1 迴圈到 n - 1,j 總是比 i 大 1,由於我們在初始化中已經把所有長度為1的情況,即dp[i][i]賦值為0,所以k迴圈裡不會用到沒有算過的值。