1. 程式人生 > >劍指Offer——動態規劃演算法

劍指Offer——動態規劃演算法

劍指Offer——動態規劃演算法

什麼是動態規劃?

     和分治法一樣,動態規劃(dynamic programming)是通過組合子問題而解決整個問題的解。

     分治法是將問題劃分成一些獨立的子問題,遞迴地求解各子問題,然後合併子問題的解。

     動態規劃適用於子問題不是獨立的情況,也就是各子問題包含公共的子子問題。

     此時,分治法會做許多不必要的工作,即重複地求解公共的子問題。動態規劃演算法對每個子問題只求解一次,將其結果儲存起來,從而避免每次遇到各個子問題時重新計算答案。

適用範圍

     最優性原理體現為問題的最優子結構特性。當一個問題的最優解中包含了子問題的最優解時,則稱該問題具有最優子結構特性。

     最優性原理是動態規劃的基礎。任何一個問題,如果失去了這個最優性原理的支援,就不可能用動態規劃設計求解。

     1.問題中的狀態滿足最優性原理。

     2.問題中的狀態必須滿足無後效性。

     所謂無後效性是指:“下一時刻的狀態只與當前狀態有關,而和當前狀態之前的狀態無關,當前狀態是對以往決策的總結”。

動態規劃演算法的設計

     兩種方法:

     自頂向下(又稱記憶化搜尋、備忘錄):基本上對應著遞迴函式實現,從大範圍開始計算,要注意不斷儲存中間結果,避免重複計算

     自底向上(遞推):從小範圍遞推計算到大範圍

動態規劃的重點

    遞迴方程+邊界條件

爬樓梯問題

     一個人每次只能走一層樓梯或者兩層樓梯,問走到第80層樓梯一共有多少種方法。

     

設DP[i]為走到第i層一共有多少種方法,那麼DP[80]即為所求。很顯然DP[1]=1, DP[2]=2(走到第一層只有一種方法:就是走一層樓梯;走到第二層有兩種方法:走兩次一層樓梯或者走一次兩層樓梯)。同理,走到第i層樓梯,可以從i-1層走一層,或者從i-2走兩層。很容易得到:

      遞推公式:DP[i]=DP[i-1]+DP[i-2]

      邊界條件:DP[1]=1   DP[2]=2

      (a)自頂向下的解法:

 

long long dp[81] = {0};/*用於儲存中間結果否則會重複計算很多重複的子問題*/
long long DP(int n)
{
    if(dp[n])
        return dp[n];
    if(n == 1)
        return 1;
    if(n == 2)
        return 2;
    dp[n] = DP(n-1) + DP(n-2);
    return dp[n];
}  

 

     (b)自底向上的解法:

 

int i;  
long long dp[81]; /* 注意當n超過75時,結果值將超過int範圍 */  
dp[1] = 1;  
dp[2] = 2;  
for(i=3; i <= 80; i++)  
    dp[i] = dp[i-1] + dp[i-2];

 

最長上升子序列

     對於序列:4 1 2 24,它的最長上升子序列是1 2 4,長度為3。

     對於序列:4 2 4 25 6,它的最長上升子序列是2 4 5 6,長度為4。

     設a[i]表示原序列,設DP[i]表示以第i個數結尾的最長上升序列的長度,那麼很顯然想匯出DP[i]的值,需要在DP[k](1<=k<i)中找出滿足a[k]<a[i]最大的一項。假設第kk項是我們找到的答案,那麼第i個數就可以接在第kk個數之後,成為以第i個數結尾的最長升序列。如果沒有找到答案,換言之第i個數比前面的數都要小,那麼DP[i]=1,也即生成了從自己開始又以自己結尾的最長升序列。綜上,我們很容易得出:

     遞推公式:DP[i]=max(DP[k]+1,DP[i])  1<=k<i

     邊界條件:DP[i]=1                   1<=i<=n

     演算法複雜度為O(n^2)

 

void RiseSequence(int Array[], int num)  
{  
#define MAX_LENGTH  30  
    struct  
    {  
        int SequenceValue;  /* max length ending with this num */  
        int PreviousIndex;  /* record the previous number */  
    }ArrayInfo[MAX_LENGTH], temp;  
    int i;  
    for(i = 0; i < num; i++)  
    {  
        int j;  
        ArrayInfo[i].SequenceValue = 1;  
        ArrayInfo[i].PreviousIndex = -1;  
        for(j = 0; j < i; j++)  
        {  
            if(Array[j] < Array[i] && (ArrayInfo[j].SequenceValue + 1 > ArrayInfo[i].SequenceValue))  
            {  
                ArrayInfo[i].SequenceValue = ArrayInfo[j].SequenceValue + 1;  
                ArrayInfo[i].PreviousIndex = j;  
            }  
        }  
    }  
    temp.SequenceValue = ArrayInfo[0].SequenceValue;  
    for(i = 1; i < num; i++)  
    {  
        if(temp.SequenceValue < ArrayInfo[i].SequenceValue)  
        {  
            temp.SequenceValue = ArrayInfo[i].SequenceValue;  
            temp.PreviousIndex = i;  
        }  
    }  
    for(i = 0; i < temp.SequenceValue; i++)  
    {  
        printf("%d  ", Array[temp.PreviousIndex]);  /* in reverse order */  
        temp.PreviousIndex = ArrayInfo[temp.PreviousIndex].PreviousIndex;  
    }  
    printf("\nthe max rising sequence length is %d\n", temp.SequenceValue);  
}  

 

最長公共子序列

     給定兩個序列X和Y,稱序列Z是X和Y的公共子序列如果Z既是X的一個子序列,又是Y的一個子序列。例如,如果X={a,b,c,b,d,a,b} Y={b,d,c,a,b,a} 那麼序列{b,c,a}就是X和Y的一個公共子序列,但是它並不是X和Y的最長公共子序列,因為它的長度為3。而同為X和Y公共子序列的{b,c,b,a},長度為4,因為找不到長度為5或更大的公共子序列,所以X和Y的最長公共子序列長度就為4。

     假設兩個序列陣列分別為a,b。定義f(i,j)為計算到a陣列第i個數、b陣列第j個數時所得到的最長公共子序列的長度。這時有兩種情況:

     1.假如a[i]=b[j],那麼f(i,j)=f(i-1,j-1)+1

     2.假如a[i]!=b[j],那麼f(i,j)=max(f(i-1,j),f(i,j-1))

     邊界條件為:f(i,0)=0     1<=i<=len(a)

            f(0,j)=0     1<=j<=len(b)

     演算法複雜度:O(n^2),len(a)表示陣列a的長度。

尾聲

     動態規劃絕對不是一兩篇文章可以講清楚的。當然也不是通過一兩道題目可以完全學會。學習的關鍵是用動規的思想去想問題,去設計狀態轉移方程式。

動態規劃還有很多變形,如狀態壓縮,樹形等等。

    雖然通常我們用遞迴的方式分析動態規劃問題,但最終都會基於迴圈去編碼。

美文美圖

 

再分享一下我老師大神的人工智慧教程吧。零基礎!通俗易懂!風趣幽默!還帶黃段子!希望你也加入到我們人工智慧的隊伍中來!https://www.cnblogs.com/captainbed