經典動態規劃引例--矩陣鏈相乘
這個感覺有必要說一下,因為很多經典的問題都是以它為根基擴充套件的,譬如:石子合併型別的。
給定由n個要相乘的矩陣構成的序列:<A1, A2, A3···An>,要計算乘積:A1*A2*A3*····An ---- <1>為了計算<1>式乘積,我們知道矩陣相乘是滿足結合律的,故無論怎麼新增括號,都會產生相同的結果。例如:矩陣鏈<A1, A2, A3, A4>乘積A1*A2*A3*A4可用五種不同的方式新增括號:
(A1*(A2*(A3*A4)))
(A1*((A2*A3)*A4))
((A1*A2)*(A3*A4))
((A1*(A2*A3))*A4)
(((A1*A2)*A3)*A4)
矩陣鏈新增括號的方式,對矩陣的運算有很大的影響,首先看一下兩個矩陣相乘的代價:
Matrix Multi_Matrix(Matrix a, Matrix b, int n) { 1 Matrix c; 2 int i, j, k; 3 for(i = 0; i < row(A); ++i) 4 { 5 for(j = 0; j < col(B); ++j) 6 { 7 c.iMatrix[i][j] = 0; 8 for(k = 0; k < col(A); ++k) 9 { 10 c.iMatrix[i][j] ^= (a.iMatrix[i][k] & b.iMatrix[k][j]); 11 } 12 } 13 } 14 return c; }
我們都直到如果兩個矩陣是相容的(即A的列數等於矩陣B的行數)時,才可以進行運算,設矩陣A,B分別為p*q矩陣,q*r矩陣,則結果矩陣C為p*r的,計算C的時間由第10行所決定的,運算次數為:p*q*r
到此,為說明矩陣鏈不同的加括號方式所帶來的矩陣乘積代價是不同的,現以下為例:
設有四個矩陣A1,A2,A3,A4,它們的維數分別是:
A1:p1×p2=50×10,A2:p2×p3=10×40
A3:p3×p4=40×30,A4:p4×p5=30×5
因此最優的新增括號順序,會為運算帶來更多的回報。那麼如何新增括號呢?即最優的斷開位置(在斷開位置新增括號,問題劃歸為兩個子問題)在哪兒呢?
如果列舉每一個加括號的位置:
設P(n)表示n個矩陣鏈相乘的方案數,則當n = 1時, 是一個平凡問題,只有一個矩陣,則P(n) = 1;當n>=2時,是一個非平凡問題,當在第k個位置新增括號時,則有P(k)*P(n-k)種方案數,由於k的位置是在[1, n-1]之間因此有如下遞推公式:
這個解是一個Catalan數序列,解的個數是關於n的指數形式,因此暴力不是一個好的方法。
步驟一:
尋找最優子結構,利用子問題的最優解來求解原問題的最優解,對於矩陣鏈相乘,我們記:A[i··j]表示A[i]*A[i+1]*····A[j]的乘積結果,其中Ai的維數表示為p[i]*p[i+1];
由於矩陣相乘滿足結合律,那麼計算A[i]*····A[j],可以在A[k]和A[k+1]之間分開,即首先計算A[i···Ak] = A[i]*A[i+1]*····A[k]和A[k+1···j] = A[k+1]*A[k+2]*···A[j]然後計算兩者相乘,這樣就可以求出A[i]*A[i+1]*···A[j], 其中k的可能位置:i <= k < j;如此,A[i]*A[i+1]*···A[j]的計算代價,就是計算A[i]*A[i+1]*···A[k]與A[k+1]*A[k+2]*····A[j]代價之和,然後再加上兩者相乘的代價。
那麼如果A[i]*A[i+1]*···A[j]的一個最優加括號方式是在A[k]和A[k+1]處分開,那麼對於它的“字首”A[i]*A[i+1]*···A[k]的最優加括號方式也一定是最優的,為什麼呢?如果A[i]*A[i+1]*···A[k]存在一個更優的加括號順序,那麼把它帶入到A[i]*A[i+1]*···A[j]中,則A[i]*A[i+1]*···A[j]的計算代價一定比最優加括號順序的代價小,矛盾了!同理[k+1]*A[k+2]*······A[j]的加括號順序也一定是最優的。這樣矩陣鏈相乘就滿足了最優子結構性質。
步驟二:
找出遞推公式:首先交代一下三個陣列的含義:m[i][j]記錄的是Ai乘到Aj的最小代價,s[i][j] = k表示A[i]*A[i+1]*···A[j]的最佳斷開位置為k,p[i]表示矩陣Ai的行數,p[i+1]表示它的列數。
假設A[i]*A[i+1]*···A[j]的最優斷開位置為k,那麼A[i]*A[i+1]*····A[j]記為:m[i][j];則:A[i]*A[i+1]*····A[k]的最小計算代價為:m[i][k];[k+1]*A[k+2]*····A[j]的計算代價記為:m[k+1][j].而A[i···k]*A[k+1····j]可表示為:p[i]*p[k+1]*p[j+1]。則m[i][j] = m[i][k] + m[k+1][j] + p[i]*p[k+1]*p[j+1]。
由於實際上k的位置是在[i, j-1]之間,因此k的位置是不確定的,那麼可以得出遞推公式:
步驟三:
如果利用遞迴求解,那麼可能會處理很多相同的子問題,那麼處理重疊子問題,也是動態規劃一個特性,例如:
考慮四個矩陣的連乘積A1A2A3A4,其中
A1:p1×p2, A2:p2×p3, A3:p3×p4, A4:p4×p5
上圖是利用自頂向下的方式求解,會有很多重疊的子問題,多次處理,從而降低了效率,如:m[2][3],m[3][4]處理了兩次,等等,如果規模更大的話,重疊的子問題也會隨之而增加。
因此我們利用自底向上的方式來求解問題,首先處理長度為1的矩陣鏈,即只有一個矩陣,有n個,那麼相乘的次數為0,即:m[i][i] = 0,1<=i<=n;
長度為2的矩陣鏈,即只有兩個矩陣相乘例如:A1*A2, A2*A3,····,Ai*Aj,····,A[n-1]*A[n],共有n-1 = n-2+1個,j = i+2-1.
·····································
度為r的矩陣鏈,有n-r+1個。
A1*A2*.......*Ar,A2*A3*.....*A[2+r-1],..............A[i]*A[i+1]*......*A[i+r-1],........
····················
顯然長度為n的只有一個,即:A[1][n],那麼m[1][n]也為最終的求解。
到此,理論完事。
程式碼:
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
using namespace std;
const int MAXN = 110;
int min_cost[MAXN][MAXN], opti_split_point[MAXN][MAXN];
int div1[MAXN];
void FindOptiSplitPoint(int n)
{
for(int i = 0; i <= n; ++i)
{
for(int j = 0; j <= n; ++j)
{
if(i == j)
min_cost[i][j] = 0;
else
min_cost[i][j] = INT_MAX;
opti_split_point[i][j] = 0;
}
}
for(int r = 2; r <= n; ++r)
{
for(int i = 1; i <= n-r+1; ++i)//總共有n-r+1種可能
{
int j = i+r-1; //其實位置為i,矩陣鏈長度為r的最後一個矩陣的位置為i+r-1
for(int k = i; k < j; ++k)
{
int t = min_cost[i][k] + min_cost[k+1][j] + div1[i]*div1[k+1]*div1[j+1];
if(t < min_cost[i][j])
{
min_cost[i][j] = min_cost[i][k] + min_cost[k][j] + div1[i]*div1[k]*div1[j];
opti_split_point[i][j] = k;
}
}
}
}
}
void Add(int i, int j)
{
if(i == j)
{
printf("A[%d]", i);
return ;
}
printf("(");
Add(i, opti_split_point[i][j]);
printf("*");
Add(opti_split_point[i][j]+1, j);
printf(")");
}
int main()
{
int n;
while(cin>>n)
{
memset(div1, 0, sizeof(div1));
for(int i = 1; i <= n+1; ++i)
cin>>div1[i];
FindOptiSplitPoint(n);
Add(1, n);
cout<<endl;
}
return 0;
}
/**
測試樣例:
6
6 7 3 1 2 4 5
輸出:
(A[1]*(A[2]*(A[3]*(A[4]*(A[5]*A[6])))))
4
50 10 40 30 5
輸出:
(A[1]*(A[2]*(A[3]*A[4])))
*/