1. 程式人生 > >我的總結-動態規劃(DP)

我的總結-動態規劃(DP)

        學習演算法已經有1年半左右了。學演算法刷題必不可少,剛開始的時候遇到題出不了只是坐那苦想,然後某一天得知上網可以搜到解題報告,興沖沖開啟網頁,在百度搜索框裡貼上了題目名字,回車,確實有各種題解。當時年少不懂網路的強大還感嘆了一把。點開之後發現,幾乎所有解題報告都是以個人部落格的名義貼出來的,原創的有,不過雷同的居多。忽略這些細節,當時確實很有一種自己什麼時候也能寫部落格的想法。畢竟人都是希望自己上進的,此外,也算是自己以後偶爾回憶過去的原材料。

        學校能有ACM集訓隊伍,也算是我的幸運。剛開始報名是幹勁十足的,很曲折地堅持著進去了才發現自己確實很有不足,在學校範圍內尚且如此,放到了外面則更是渺小。基本演算法的學習只一年不到教練就講完了。基本上只是帶入門,資質略淺的我聽得根本就是雲裡霧裡,然後專題也是靠著題解的理解水了一些,然後這半年多就這麼過去了。期間零零散散又補了一些知識,不過還是不夠深入,我想,寫部落格這種東西或許能帶動思維,加深理解。

        說到動態規劃,最開始接觸到這型別的題目是在教練上課的時候放了杭電OJ的名為“數塔”的題目,其實僅靠著沒有任何演算法基礎而且對電腦程式設計處理問題的方式還不熟悉只會暴力加模擬的思維方式,著實沒有除開暴力之外的其它方法。所以即使是現在看來這樣簡單的題目,也能難住很多初學者,並且DP確實很靈活,而且不好理解。不過萬事開頭難,理解了一些題目,慢慢也能看懂題解並自己做題了。經過我自己這段時間的搗鼓,總結了自己的一些東西。

        動態規劃是演算法裡面比較特殊的一類,沒有特定的模板,沒有特定的知識點,能說的方法也很少。總的來說,動態規劃類題目有兩個特點:一是問題包含最優子結構;二是子狀態可以重複取到。和搜尋一樣,是計算機擅長而人不擅長的處理問題的方式。所以一開始理解起來確實多有不便。說回來,應這兩個特點,動態規劃的要素有二:一是狀態方式的選取;二是狀態轉移方程或轉化公式。其中前者是關鍵,也是難點,因為狀態選出來了後者不難得到的,甚至有時候能想出前者是因為先想出了後者,並且想出每一個方案都得再想怎樣把每個狀態都迴圈取到,並證明是否具有最優子結構性質……所以經過這麼些題目的“洗禮”,我算是摸到了其冰山一角,記下一些這些天來的訓練題目與思考方式。

核心程式碼:

for (int i = n - 2; i >= 0; i--)
{ 
    for (int j = 0; j <= i; j++)
    {
         dp[i][j] = max(a[i][j] + dp[i + 1][j], a[i][j] + dp[i + 1][j + 1]);   /*子狀態由所有可以到達此狀態的上一狀態更新*/
    }
}

答案即是數塔最頂端dp[0][0],這種可以很容易得到狀態表示的動態規劃算是最簡單入門的一種了,而且本題還是直接給出了提示性的資料結構,只要能有最優子結構的概念就能出。

核心程式碼:

for (int i = T - 1; i >= 0; i--)
{
    for (int j = 0; j <= 10; j++)   /*下面是子狀態由所有可以到達此狀態的上一狀態的更新過程*/
    {
        if (j != 0)
            dp[i][j] = max(dp[i][j], dp[i + 1][j - 1] + value[i + 1][j - 1]);
        if (value[i + 1][j])
            dp[i][j] = max(dp[i][j], dp[i + 1][j] + value[i + 1][j]);
        if (j != 10)
            dp[i][j] = max(dp[i][j], dp[i + 1][j + 1] + value[i + 1][j + 1]);
     }
}

答案為dp[0][5],即最開始座標為5位置的答案,這一題和上一題一樣,都是逆序列舉,可以保證最後結果不用再選。這依然是可以比較容易得出狀態表示的一種,因為可以比較容易畫出它的狀態轉換DAG(有向無環)圖,同一狀態有多種不同方式到達,典型的需要轉化公式的動態規劃題。

核心程式碼:

for (i = 1; i < n; i++)
{
    if (a[i] + sum >= a[i])  
    {
        sum = a[i] + sum;
    }
    else
    {
        sum = a[i];
        b = i;
    }
    if (sum > maxsum)
    {
        maxsum = sum;
        end = i;
        beg = b;
    }
}
一開始我不知道這是動態規劃類的題目,只是覺得方法比較巧妙,因為學習用課件上的版本,這一句
if (a[i] + sum >= a[i]) 
{
    sum = a[i] + sum;
}
被簡化成了
if (sum >= 0) 
{
    sum = a[i] + sum;
}

因而看不出動態規劃標誌性的具有轉移方程或轉化公式的特點,重做這道題的時候自然而然寫出了簡化之前的程式碼。思路就是留下前面對後續累加有幫助的串,即加上之後比沒加上大,若到某位置此條件不成立,則另起起點。同時要注意,最終的最大值可能在任何兩起點之間產生。

核心程式碼:

for (int i = 0; i < a.length(); i++)
{
    for (int j = 0; j < b.length(); j++)
    {
        if (a[i] == b[j])
            dp[i + 1][j + 1] = dp[i][j] + 1;    /*串1與串2當前位置字元相等,子狀態可以更新*/
        else
            dp[i + 1][j + 1] = max(dp[i][j + 1], dp[i + 1][j]); /*串1與串2當前位置字元不相等,子狀態沿用可以到達次狀態的最值*/
    }
}
這個狀態表示方式若還有意用頭兩個題一樣的思維方式的話會發現很難得出,因為它的狀態不再單單是以某一步為下標的DAG圖,而是以它自身子結構的狀態。要算dp[I][J],可以從i:0->I,j:0->J,依次求出其子結構的最值,下一個子結構的最值則可以很方便由前面已知的子結構最值得到。

核心程式碼:

for (int i = 0; i < n; i++)  /*這樣遍歷可以保證每個val[i]對每個dp[j]能且僅能入選一次*/
{
    for (int j = maxkg; j >= kg[i]; j--)  /*dp[j]表示總量為j時的最值*/
    {
        dp[j] = max(dp[j - kg[i]] + val[i], dp[j]);  /*更新狀態*/
    }
}

這個狀態的表示方法與上題比較類似,要求最終結果,先求其子結構,但是又多了另一種方式的轉移方程,即更新dp[j]時不一定只用到的是上一個狀態。

核心程式碼:

for (i = 0; i < m; i++)  /*對於每個mon[i]有選與不選的自由*/
{
    for (int j = n; j>=mon[i]; j--)  /*dp[j]表示總量為j時的最值*/
    {
        dp[j] = min(dp[j], dp[j - mon[i]] * pro[i]);  /*更新狀態*/
    }
}

與上題是一個型別,只是轉移方程略有不同。其實這類題歸結於0-1揹包,是程式碼簡化後的版本。上題有說”保證每個val[i]對每個dp[j]能且僅能入選一次“,是與每個val[i]有無限個可選類的題目區分開。