動態規劃(dynamic programming)

  動態規劃(dynamic programming)是一種高效的程序設計技術,通常應用與最優化問題,即當咱們面臨多組選擇時,選擇一個可行解讓問題達到最優。動態規劃的一個顯著特色是:原問題能夠劃分紅更小的子問題的最優化問題,而這些子問題的解每每有着重疊的部分。ios

  動態規劃算法的解決一個問題,能夠分紅四個步驟:算法

  1)描述最優解結構,需找最優子結構編程

  2)遞歸定義最優值的解數組

  3)自底向上的求解問題函數

  4)依據計算過程,構造一個最優解優化

  1)與 2)是這個問題能夠用動態規劃解決的理論基礎,3)能夠看出是動態規劃算法的編程實踐,4)是算法輸出,依賴於3)。下面分別用三個經典的動態規劃案例,闡釋動態規劃算法的用法。spa

  案例一:矩陣連乘設計

  問題描述:給定n個矩陣{A1,A2,…,An},其中AiAi+1是可乘的,i=1,2,…,n-1。考察這n個矩陣的連乘積A1A2…An。因爲矩陣乘法知足結合律,故計算矩陣的連乘積能夠有許多不一樣的計算次序,這種計算次序能夠用加括號的方式來肯定。若一個矩陣連乘積的計算次序徹底肯定,則能夠依這次序反覆調用2個矩陣相乘的標準算法計算出矩陣連乘積。A是一個p×q矩陣,B是一個q×r矩陣,則計算其乘積C=AB的標準算法中,須要進行pqr次數乘。矩陣連乘積的計算次序不一樣,計算量也不一樣code

  Example:先考察3個矩陣{A1,A2,A3}連乘,設這三個矩陣的維數分別爲10×100100×55×50blog

  • 若按((A1A2A3)方式須要的數乘次數爲10×100×510×5×507500
  • 若按(A1A2A3))方式須要的數乘次數爲100×5×5010×100×5075000

  解決方案:動態規劃

  1)描述最優解結構,尋找最優子結構

  記AiAi+1…Aj爲A[i:j],考察A[1:n]的最優計算次序問題:假設這個計算次序在k(1<=k<=n)處斷開,那麼A[1:k]和A[k+1:n]兩個子序列的中的計算次序也是最優的。

  爲證實子結構與原問題也是一個相同的最優問題,通常採用反證法:

  若是A[1:k]或者A[k+1:n]不是最優的,那麼能夠必然能夠找到一個新的計算次序將A[1:k]或是A[k+1:n]替換,新的計算序列須要的計算次數更少,但這與A[1:n]是最有解序列矛盾。

  2)遞歸定義最優值

  令m[i][j]表示A[i:j]最小的計算次數,那麼遞歸定義的表達式以下:

  3)自底向上的求解

  有了2)的遞歸表達式,,代碼實現將會變得簡單,代碼以下:

/************************************************************************/
/* p: 輸入參數,存儲矩陣序列的中行列值
 * m: m[i][j], 存放A[i:j]的計算次數
   s: s[i][j], 記錄A[i:j]斷開的位置
*/
/************************************************************************/
int matrix_chain(int* p, int n, int** m, int** s)
{
    for (int i = 0; i != n; i++)
    {
        m[i][i] = 0;
    }

    for (int r = 2; r <= n; r++)
    {
        for (int i = 0; i <= n-r; i++)
        {
            int j = i + r - 1;
            m[i][j] = m[i+1][j] + p[i]*p[i+1]*p[j+1];               //從i處斷開
            s[i][j] = i;
            
            for (int k = i+1; k <= j; k++)
            {
                int t = m[i][k] +m[k+1][j] + p[i]*p[k+1]*p[j+1];   //從k處斷開
         if (t< m[i][j]) 
         {
           m[i][j]
= t;
           s[i][j]
= k;
         }
       }
     }
  }
}

  4)構造最優解

  在第三步自底向上的求解過程當中,記錄了構建最優解的最優的必要形式(A[i:j]該斷開的位置),構造最優解的過程以下:

void trace_back(int** s, int i, int j)
{
    if (i == j)
    {
        return;
    }

    trace_back(s, i, s[i][j]);
    trace_back(s, s[i][j] + 1, j);

    cout<<"A("<<i<<","<<s[i][j]<<")"<<"\t";
    cout<<"Multiply\t"<<"A("<<s[i][j]+1<<","<<j<<")"<<endl;
}

  程序的主函數以下:

int main()
{
    const int n = 6;
    int p[] = {30, 35, 15, 5, 10, 20, 25};
    int **m, **s;

    m = new int*[n];
    for( int i = 0; i < n; i++)
        m[i] = new int[n];

    s = new int*[n];
    for(int i=0; i<n; i++)
        s[i] = new int[n];  

    matrix_chain(p, n, m, s);
    trace_back(s, 0, n-1);

    for(int i=0;i<n;i++)  
    {  
        delete []m[i];
        delete []s[i];
    }  
    delete []m;   
    delete []s;  

    system("pause");
    return 0;
}

  案例二:最長公共子序列

  問題描述:子序列是指,在原序列中刪除若干元素後所得的序列。公共子序列是指,給定兩個序列X和Y,另外一個序列Z既是X的子序列又是Y的子序列,那麼Z則被稱爲X與Y的公共子序列。而最長公共子序列,則是求X與Y最長的子序列中長度最大的一個序列。

  Example: X = {A,B, C, B, D, A, B}, Y = {B, D, C, A, B, A},他們的一個最長公共子序列是Z = {B, C, B, A }

  解決方案:動態規劃

  步驟一:描述最優解結構,尋找最優子結構

  設序列X = {x1, x2,....xn}, 序列Y = {y1, y2,...ym},它們的最長公共子序列是Z = {z1,z2, ...zk},他們之間有以下的最優結構性質:

  1)若xn=ym,則zk=xn=ym,zk-1將是Xn-1與Yn-1的最長公共子序列

  2)若xn=ym且zk≠xn,那麼Z是Y與Xn-1最長公共子序列

  3)若xn=ym且zk≠ym,那麼Z是X與Yn-1最長公共子序列

  有關最優子結構性質,依然能夠採用反證法證實。

  2)遞歸定義最優值

  記c[i][j]表示序列Xi與序列Yj最長公共子序列的長度,其中Xi = {x1, x2,....xi}, Yj = {y1, y2,...yj},有以下的遞歸定義表達式:

  

 

  3)自底向上的求解

  依照上面的公式,自底向上的求解代碼的代碼以下:

#include <iostream>

using namespace std;

int lcs_length(char* x, int m, char* y, int n, int** c)
{
    for (int i = 0; i <= m ; i++)
        c[i][0] = 0;
    for (int j = 0; j <= n; j++)
        c[0][j] = 0;

    for (int i = 1; i <=m; i++)
    {
        for (int j = 1; j <= n; j++)
        {
            if (x[i-1] == y[j-1])             //下標從l開始
            {
                c[i][j] = c[i-1][j-1] + 1;   
            }
            else
            {
                c[i][j] =  max(c[i-1][j], c[i][j-1]);
            }
        }
    }

    return c[m][n];
}

void print_lcs(int i, int j, char* x, int** c)
{
    if (i == 0 || j == 0)
    {
        return;
    }
    
    if (c[i][j] == c[i-1][j-1] + 1)
    {
        print_lcs(i-1, j-1, x, c);
        cout<<x[i-1]<<endl;
    }
    else if (c[i][j] == c[i-1][j])
    {
        print_lcs(i-1, j, x, c);
    }
    else
    {
        print_lcs(i, j-1, x, c);
    }

}

int main()
{
    char x[] = {"ABCBDAB"};
    char y[] = {"BDCABA"};

    int m = strlen(x);
    int n = strlen(y);

    int** c = new int*[m+1];
    for (int i = 0; i<=m; i++ )
    {
        c[i] = new int[n+1];
    }

    int t = lcs_length(x, m, y, n, c);
    for (int i = 0; i <= m; i++)
    {
        for (int j = 0; j <= n; j++)
        {
            cout<<c[i][j]<< " ";
        }
        cout<<endl;
    }
    print_lcs(m, n, x, c);

    system("pause");
    return 0;
}

代碼的執行圖解以下:

  案例三:最長遞增子序列

  問題描述:求一個一維數組(N個元素)中的最長遞增子序列的長度。

  Example:在序列1-12-34-56-7中,其最長的遞增子序列爲1246

  解決方案:動態規劃

  繼續依據動態規劃的解決思想,解決過程省略,這裏直接給出最優解的遞歸結構表達式,令m(i, j)表示以i爲起點j爲終點(包括原始array[i])的子序列,增加序列的最長長度,則遞歸表達式爲:

  

  實現代碼以下:

#include <iostream>

using namespace std;

/************************************************************************/
/*
  array: 存放序列
  m: m[i][j],表示以i爲起點j爲終點(包括原始array[i])的子序列,增加序列長度
  n: array長度
*/
/************************************************************************/
int lis_length(int* array, int** m, int n)
{
    for (int i = 0; i < n; i++)
    {
        m[i][i] = 1;
    }

    for (int r = 2; r <= n; r++)
    {
        for (int i = 0; i <= n - r; i++)
        {
            int j = i + r -1;
            if (array[i] < array[i+1])
            {
                m[i][j] = m[i+1][j] + 1;
            }
            else
            {
                m[i][j] = m[i+1][j];
            }
        }
    }
    
    return m[0][n-1];
}

void print_lis(int* array, int** m, int n)
{
    int lic_len = m[0][n-1];

    //m[0][i]記錄了序列array[0:i],最長遞增加度
    //m[0][i] = k,序列第k個增加元素出現
    for (int i = 0, lic_tag = 1; i != n; i++)
    {
        if (m[0][i] == lic_tag)
        {
            cout<<array[i]<<" ";
            lic_tag += 1;
        }
    }
}

int main()
{
    const int n = 8;
    int array[n] = {1, 4, 2, -3, 4, 8, 6, -7};

    int** m = new int*[n];
    for (int i = 0; i != n; i++)
    {
        m[i] = new int[n];
    }

    int t = lis_length(array, m, n);
    print_lis(array, m, n-1);

    for (int i = 0; i != n; i++)
    {
        delete[] m[i];
    }
    delete[] m;

    system("pause");
    return 0;

}

  總結:1)可以最優子結構性質,是使用動態規劃算法的先決條件;2)重複解結構,動態規劃算法可以高效使用的緣由在於記錄了子結構的解,而這些解又會在後續的求解過程當中被咱們使用(優於遞歸算法的緣由);3)最後在編程實踐技巧上,利用了自底向上的求解技術,從子問題開始求解。

  在編程實踐的過程,特別須要主要如下問題:1)初始化,零界狀況下表格的初始化必須提早完成;2)子問題的求解必定要現優於原問題,如出現某個子問題未求解出,但算法開始處理該原問題,將會引入錯誤(錯誤將很是隱晦);3)解的信息保存。

相關文章
相關標籤/搜索