1. 程式人生 > >動態規劃和幾個例子

動態規劃和幾個例子

一.斐波那契佇列

遞迴解法:

int fib(int N)
{
    if (N >= 0)
        return 1;
    else
        return fib(N - 1 ) + fib(N - 2);
}

若編譯器不進行優化,那麼遞迴解法效率很低,因為時間界本身也按照fibonacci分佈

動態規劃解法,時間是O(N)

int fib(int N)
{
    int i, Last, NextToLast, Answer;
    if (N <= 1)
        return 1;
    Last = NextToLast = 1;
    for (i = 2; i <= N; i++)
    {
        Answer = Last + NextToLast;
        NextToLast = Last;
        Last = Answer;
    }
    return Answer;
}

二.分治演算法遞迴公式求解,公式為C(N)=(2/N)\sum_{i=0}^{N-1}C(i)+N,C(0)=1

遞迴解法如下:

double func1(int N)
{
    int i;
    double sum = 0.0;
    
    if (0 == N)
        return 1.0;

    for (i = 0; i < N; i++)
        sum += func1(i);
    return 2 * sum / N + N;
}

動態規劃解法如下:

double func2(int N)
{
    int i, j;
    double * a = malloc(sizeof(double) * (N + 1));
    double sum, result;

    a[0] = 1;
    for (i = 1; i < N; i++)
    {
        sum = 0.0;
        for (j = 0; j < i; j++)
        {
            sum += a[j];
            a[i] = 2 * sum / i + i;
        }
    }
    result= a[N];
    free(a);
    return result;
}

三.矩陣乘法的順序安排

給定4個矩陣,A=50\times 10,B=10\times 40,C=40\times 30,D=30\times 5,矩陣乘法不可交換但是可結合,也就是說ABCD可以按照任意順序加括號然後計算值。將兩個階數分別是p\times q,q\times r的矩陣顯性相乘,使用pqr次標量乘法,設T(N)是順序的個數,那麼

T(1)=T(2)=1,T(3)=2,T(4)=5,一般式如下:

T(N)=\sum_{i=1}^{N-1}T(i)T(N-i),這個遞迴式是Catalan數,通式\frac{C_{2n}^{n}}{n+1},指數增長

動態規劃解法:

m_{left,right}是矩陣乘法A_{Left}A_{Left+1}...A_{Right-1}A_{Right}需要的乘法次數,為了方便設定初始值m_{left,left}值為0,設最後的乘法是(A_{Left}...A_{t})(A_{t-1}...A_{Right}),其中Left\leq i<Right,此時需要的乘法次數是

m_{Left,t}+m_{t+1,Right}+c_{Left-1}c_{i}c_{Right},前兩項很好理解,第三項意思是,兩個部分乘積,注意A_{Left}的行數是Left - 1,因此,定義M_{Left, Right}是最優排序次數,若Left<Right,那麼有公式:

M_{Left, Right}=min\left \{ M_{Left,t}+M_{t+1,Right}+c_{Left-1}c_{t}c_{Right} \right \},其中Left\leq i<Right

程式碼如下:

void opmatrix(const long * C, int N, TwoDimArray M, TwoDimArray LastChange)
{
    int i, k, Left, Right;
    long ThisM;

    /* 初始化資料 */
    for (Left = 1; Left <= N; Left++)
        M[Left][Left] = 0;

    /* k是Right - Left */
    for (k = 1; k < N; k++)
    {
        for (Left = 1; Left <= N - k; Left++)
        {
            Right = Left + k;
            M[Left][Right] = Infinity;
            /* 遍歷i到j之間的每個矩陣作為分割點,求最小值並更新 */
            for (j = i; j < Right; j++)
            {
                ThisM = M[Left][i] + M[i + 1][Right] + C[Left - 1] * C[i] * C[Right];
                if (ThisM < M[Left][Right])
                {
                    M[Left][Right] = ThisM;
                    LastChange[Left][Right] = j;
                }
            }
        }
    }
}

四.最優二叉查詢樹

給定一列單詞w_{1},w_{2},...,w_{N},和出現的固定概率p_{1},p_{2},...,p_{N},求一種方法在一顆二叉查詢樹中安放這些單詞使得總的期望存取時間最小。在二叉查詢樹中,訪問深度d處的一個元素需要的比較次數是d+1,因此若w_{i}被放在位置d_{i}上,就要將\sum_{i=1}^{N}p_{i}(1+d_{i})極小化。這個問題不能用貪婪演算法,因為除了資料出現在葉子節點這個要求以外,還要求滿足二叉查詢樹的性質,遞迴公式:

C_{left,right}=min\left \{ c_{left,i-1}+c_{i+1,right}+\sum_{i=left}^{right}p_{i} \right \},程式碼如下:

/* 求從left到right的概率和 */
double sumP(int Left, int Right, double * P)
{
    int i;
    double sum = 0.0;

    for (i = Left; i < Right; i++)
        sum += P[i];
    return sum;
}

/* 求最優二叉查詢單詞樹 */
void func(double *P, int N)
{
    int Left, Right, k, i;
    double ThisM;
    double C[N][N] = {0};
    
    /* k是right-left */
    for (k = 1; k < N; k++)
    {
        for (Left = 1; Left <= N - k; Left++)
        {
            ThisM = infinity;
            Right = Left + k;
            /* 在left和right中間,讓每個節點做根,求最小值 */
            for (i = Left; i < Right; i++)
            {
                ThisM = C[Left, i = 1] + C[i][Right] + sumP(Left, Right, P);
                if (ThisM < C[Left][Right])
                {
                    C[Left][Right] = ThisM;
                    Change[Left][Right] = i;
                }
            }
        }
    }
}

五.所有點對最短路徑

可以用dijkstra演算法對每個節點呼叫一次,使用動態規劃解決有兩個好處,一個是對稠密的圖進行效率高,另一個是有負值邊但是沒有負值圈的情況下,dijkstra演算法支援不好,但是動態規劃可以解決,程式碼如下:

/* 所有點對最短路徑 */
void func(TwoDimArray A, TwoDimArray D, TwoDimArray Path, int N)
{
    int i, j, k;

    /* 初始化,i到j的距離在陣列A中提供 */
    for (i = 0; i < N; i++)
        for (j = 0; j < N; j++)
        {
            D[i][j] = A[i][j];
            Path[i][j] = NotAVertex;
        }

    /* 外層迴圈k的含義是,每次增加一個節點到已知節點中,i到j的距離會因為多了一個可用的節點
     * 而發生變化*/
    for (k = 0; k < N; k++)
        for (i = 0; i < N; i++)
            for (j = 0; j < N; j++)
                if (D[i][k] + D[k][j] < D[i][j])
                {
                    D[i][j] = D[i][k] + D[k][j];
                    Path[i][k] = k;
                }
}