1. 程式人生 > >矩陣鏈乘法問題 (演算法)

矩陣鏈乘法問題 (演算法)

一、概述


        以兩個矩陣相乘為例,A1*A2,A1和A2為兩個矩陣,假設A1的行列數是p*q,A2的行列數是q*r。注意這裡由於是A1乘以A2,所以A1的列數要等於A2的行數,否則無法做矩陣乘法,滿足上述條件的矩陣,我們稱之為“相容”的。那麼對於A1*A2而言,我們需要分別執行p*r次對應A1的行元素乘以A2的列元素,根據線性代數知識,不難得出我們一共需要執行p*q*r次乘法。


        對於兩個矩陣相乘,一旦矩陣的大小確定下來了,那麼所需執行的乘法次數就確定下來了。那麼對於兩個以上的矩陣呢?是不是也是這樣呢。實際上,對於多個矩陣相乘,乘法執行的次數與“劃分”有關。例如:


        以矩陣鏈<A1,A2,A3>為例,假設三個矩陣的規模分別為10X100,100X5和5X50。


        ①以((A1*A2)*A3)方式劃分,乘法執行次數為:10*100*5+10*5*50=5000+2500=7500次


        ②以(A1*(A2*A3))方式劃分,乘法執行次數為:100*5*50+10*100*50=25000+50000=75000次


        我們可以發現,對於同樣的矩陣鏈<A1,A2,A3>相乘而言,不同的劃分,乘法次數居然相差10倍。

 

二、如何獲得最佳的矩陣鏈乘法劃分和最少次數


        其實這裡與“鋼管切割”有相似之處,在鋼管切割問題中,我們使用一個一維陣列來儲存最佳收入,另一個一維陣列來儲存切割的劃分處。


        而這裡,我們也可以將矩陣鏈看成一根要分割的“鋼管”,只是這裡記錄的兩個陣列需要是二維的,因為我們需要記錄的不僅是從哪裡“斷開”,還需要記錄"每一段"到哪裡截止。如下圖:

 


       使用一個長度為n+1的一維陣列p來記錄每個矩陣的規模,其中n為矩陣下標i的範圍1~n,例如對於矩陣Ai而言,它的規模應該是p[i-1]到p[i]。由於i是從1到n取值,所以陣列p的下標是從0到n。


       用於儲存最少乘法執行次數和最佳分段方式的結構是兩個二維陣列m和s,都是從1~n取值。m[i][j]記錄矩陣鏈<Ai,Ai+1,...,Aj>的最少乘法執行次數,而s[i][j]則記錄 最優質m[i][j]的分割點k。


       需要注意的一點是當i=j時,m[i][j]=m[i][i]=0,因為一個矩陣不需要任何乘法。


       假設矩陣鏈從Ai到Aj,有j-i+1個矩陣,我們從k處分開,將矩陣鏈分為Ai~Ak和Ak+1到Aj兩塊,那麼我們可以比較容易的給出m[i][j]從k處分隔的公式:


       m[i][j]=m[i][k]+m[k+1][j]+p[i-1]*p[k]*p[j];


       在一組確定的i和j值的情況下,要使m[i][j]的值最小,我們只要在所有的k取值中,i<=k<j,尋找一個讓m[i][j]最小的值即可。


       假設L為矩陣鏈的長度,那麼L=j-i+1。當L=1時,只有一個矩陣,不需要計算。那麼我們可以從L=2到n進行迴圈,對每個合理的i和j值的組合,遍歷所有k值對應的m[i][j]值,將最小的一個記錄下來,儲存到m[i][j]中,並將對應的k儲存到s[i][j]中,就得到了我們想要的結果。

根據上面的分析,不難給出過程的程式碼,注意這裡也是使用的自底向上的方法,參看“鋼管切割”

//Ai矩陣的行列分別是p[i-1]和p[i],1<=i<=n
 
/*
 * 求解最少次數的乘法括號劃分方案
 */
void Matrix_Chain(int* p, int n, int** m, int** s) {
 
	//①將對角線上的值先賦值為0
	for (int i = 1; i <= n; i++) {
		m[i][i] = 0;
	}
 
	int l = 0; //l為矩陣鏈的長度
 
	//m[i][j]的第一個引數
		int i = 0;
 
	//m[i][j]的第二個引數
	int j = 0;
 
 
 
	int tmp = 0;
 
	//②以長度L為劃分,L從2開始到n
	for (l = 2; l <= n; l++) {
 
		//迴圈第一個引數,因為l的長度至少為2,所以i和j在這個迴圈裡面肯定不相等
		for (i = 1; i <= n - l + 1; i++) {
			//因為j-i+1=l,所以j=l+i-1
			j = i + l - 1;
 
			//給m[i][j]賦初值,這裡要尋找m[i][j]的最小值,本來應當給m[i][j]賦值一個正無窮,但是這裡直接賦一個i=j時候的特值也可以
			m[i][j] = m[i][i] + m[i + 1][j] + p[i - 1] * p[i] * p[j];
			s[i][j] = i;
 
			//對於每個特定的i和j的組合,遍歷此時所有的合適k值,k大於等於i小於j
			for (int k = i + 1; k < j; k++) { //這裡k不能等於j,因為後面要m[k+1][j],不然k+1就比j大了
 
				tmp = m[i][k] + m[k + 1][j] + p[i - 1] * p[k] * p[j];
 
				if (tmp < m[i][j]) {
					m[i][j] = tmp;
					s[i][j] = k;
				}
			}
		}
	}
}

上面的程式碼,我們就求得了每種i和j組合對應的最小乘法次數和對應的最佳分割處s[i][j]。

三、輸出最優構造劃分:

 

       經過執行上面的程式碼,我們就準備好了s[i][j],其中包含最佳分割資訊。我們可以使用一種類似於中序遍歷的方法來輸出劃分方式,比如對<A1,A2,A3,A4,A5>和他們對應的下標陣列p而言。

void print_optimal_parens(int** 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 << ")";
	}
}

比如對於陣列p={5,6,2,9,7,6}和<A1,A2,A3,A4,A5>經過上面兩段程式碼的呼叫,輸出劃分結果:

 

((A1A2)((A3A4)A5))

 

最少乘法次數為:330次

備註:

動態規劃是一種將問題例項分解為更小的、相似的子問題,並存儲子問題的解而避免計算重複的子問題,以解決最優化問題的演算法策略。

備忘錄法是動態規劃方法的變形。與動態規劃演算法不同的是,備忘錄方法的遞迴方式是自頂向下的,而動態規劃演算法則是自底向上的。

 

轉載連結:

https://blog.csdn.net/cyp331203/article/details/42965237