1. 程式人生 > >經典動態規劃引例--矩陣鏈相乘

經典動態規劃引例--矩陣鏈相乘

  這個感覺有必要說一下,因為很多經典的問題都是以它為根基擴充套件的,譬如:石子合併型別的。    

  給定由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])))
*/