1. 程式人生 > >動態規劃 (二) 最優矩陣鏈乘

動態規劃 (二) 最優矩陣鏈乘

背景分析

最優矩陣鏈乘是二維的動態規劃問題,也是較為經典的動態規劃入門問題。《演算法導論》和劉汝佳的《演算法競賽入門經典》中都有詳細描述。

問題描述

線上性代數裡,我們都學過矩陣的乘法。矩陣的乘法不滿足分配律,但是滿足結合律,因此 (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個矩陣 A
是pi-1 X pi的。

思路分析

我們先把這n個矩陣 AX AX AX ...... X An分成兩份,P = AX AX ... 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迴圈裡不會用到沒有算過的值。

動手時間

假如都理解了上面的程式碼,那麼可以去OJ上A一下這個題目了,基本上就上面的程式碼稍微改一下就行,連結如下:POJ 1651