LeetCode總結,動態規劃問題小結

EbowTang發表於2016-03-04

一,參考一般書籍中的“動態規劃”講解

1、基本概念

動態規劃(Dynamic Programming)對於子問題重疊的情況特別有效,因為它將子問題的解儲存在表格中,當需要某個子問題的解時,直接取值即可,從而避免重複計算!
動態規劃是一種靈活的方法,不存在一種萬能的動態規劃演算法可以解決各類最優化問題(每種演算法都有它的缺陷)。所以除了要對基本概念和方法正確理解外,必須具體問題具體分析處理,用靈活的方法建立數學模型,用創造性的技巧去求解。

2、基本思想與策略

    基本思想與分治法類似,也是將待求解的問題分解為若干個子問題(階段),按順序求解子階段,前一子問題的解,為後一子問題的求解提供了有用的資訊。在求解任一子問題時,列出各種可能的區域性解,通過決策保留那些有可能達到最優的區域性解,丟棄其他區域性解。依次解決各子問題,最後一個子問題就是初始問題的解。
動態規劃中的子問題往往不是相互獨立的(即子問題重疊)。在求解的過程中,許多子問題的解被反覆地使用。為了避免重複計算,動態規劃演算法採用了填表來儲存子問題解的方法。


 


3、適用的情況

1)兩個必備要素

適合應用動態規劃方法求解的最優化問題應該具備兩個重要的要素:最優子結構和子問題重疊。

(a)最優子結構:問題的最優解由相關子問題的最優解組合而成,並且可以獨立求解子問題!

(b)子問題重疊:遞迴過程反覆的在求解相同的子問題。


2)三個性質

能採用動態規劃求解的問題的一般要具有3個性質:

(a) 最優化原理:如果問題的最優解所包含的子問題的解也是最優的,就稱該問題具有最優子結構,即滿足最優化原理。

(b) 無後效性:即某階段狀態(定義的新子問題)一旦確定,就不受這個狀態以後決策的影響。也就是說,某狀態以後的過程不會影響以前的狀態,只與其以前的狀態有關。

(c)有重疊子問題:即子問題之間是不獨立的(分治法是獨立的),一個子問題在下一階段決策中可能被多次使用到。(該性質並不是動態規劃適用的必要條件,但是如果沒有這條性質,動態規劃演算法同其他演算法相比就不具備優勢)


4、求解的基本步驟

實際應用中可以按以下幾個簡化的步驟進行設計:

    (1)分析最優解的性質,並刻畫其結構特徵,這一步的開始時一定要從子問題入手。

    (2)定義最優解變數,定義遞迴最優解公式。

    (3)以自底向上計算出最優值(或自頂向下的記憶化方式(即備忘錄法))

    (4)根據計算最優值時得到的資訊,構造問題的最優解


二,動態規劃的自我總結

實際上動態規劃的問題就是在犧牲一定量的記憶體儲存子問題的計算結果,從而未來需要這些資訊時不再重複計算,直接獲取計算過的結果即可。其實動態規劃的問題不好想,除非遞推公式比較明顯。

1,動態規劃問題的判斷

那麼什麼樣的問題是動態規劃呢?

比如如下幾個經典問題

<LeetCode OJ> 5. Longest Palindromic Substring

<LeetCode OJ> 53. Maximum Subarray

<LeetCode OJ> 62. / 63. Unique Paths(I / II)

<LeetCode OJ> 64. Minimum Path Sum

<LeetCode  OJ> 70. Climbing Stairs

<LeetCode OJ> 96. Unique Binary Search Trees

<LeetCode OJ> 120. Triangle

<LeetCode OJ> 121. /122. Best Time to Buy and Sell Stock(I/II)

<LeetCode OJ> 123. / 188. Best Time to Buy and Sell Stock (III / IV)

這一系列的問題都是最優值問題,能從題目中露骨的感受到求取最優結果的意思

比如最長迴文...最長上升序列...等等。實際上刷題刷多了之後一看就知道是動態規劃問題。


2,歸納設計步驟

實際應用中可以按以下幾個簡化的步驟進行設計:

    (1)定義子問題變數,分析遞迴最優解的公式。

              思考問題時一般都是將問題的規模縮小,即較小的子問題,假定其已經獲得了最優值,然後將其擴大一點點成為較大規模的子問題,那麼較大規模子問題應該怎樣從較小規模的子問題遞推公式得到呢?並且使得當前的子問題也是最優值。

    (2)分析最優解的性質,並刻畫其結構特徵。

                說白了就是刻意尋找定義的子問題變數與前面更小規模子問題變數的關係,一般用一個關係式表達。

    (3)以自底向上計算出最優值         

               所謂自底向上的遞推過程,即由較小規模的子問題遍歷到原問題。首先我們的原問題是什麼?那麼他的較小規模的子問題是什麼?然後思考我們應該怎麼樣遍歷才能遍歷回原問題(首先思考遍歷的最外層)。在以後的遍歷過程就是通過某種遍歷順序遍歷回原問題。比如,最長迴文字串,我們定義的子問題就是“dp[i][j] 表示子串s[i…j]是否是迴文”。子串s[i…j]就是原問題的縮小版本,因為我們始終可以通過控制兩個變數變回原問題。

    (4)實際編寫程式時一定要記得初始化

          有的邊界問題可能不能由遞推公式獲得。比如最長迴文字串,必須先初始化,這一步一定要納入思考的範圍。


三,分析幾個經典的動態規劃例子

例子1

最長迴文字串

<LeetCode OJ> 5. Longest Palindromic Substring

說真的這個動態規劃問題的子問題不好一下想到,為什麼不是“dp[i] 表示子串s[0…i]是否是迴文”呢?恐怕要經過多番假設子問題才能最終找到正確的子問題設定。

定義子問題:dp[i][j] 表示子串s[i…j]是否是迴文,我們這樣定義實際上變相知道了當前迴文子串的長度,以及在原字串中的位置。
1,初始化:
1),dp[i][i] = true (0 <= i <= n-1); 
2),if(s[i]==s[i+1]), dp[i][i+1] = true (0 <= i <= n-2); 
3),其餘的初始化為false

2,在初始化基礎上的遞推過程
如果子問題dp[i+1][j-1] == true,並且擴張一個位置後s[i] == s[j] 
顯然當前位置,dp[i][j] = true,否則還是為false(意義就是,小的子串都不是迴文,在此基礎上更大的子串也不是迴文)在動態規劃中更新最長迴文的長度及起點以及長度即可

 

3,自底向上的遍歷

從大的方向上來考慮:動態規劃是自底向上的遞推過程,即由較小規模的子問題遍歷到原問題。

首先我們的原問題是什麼?在給定字串s中尋找最長的迴文。

那麼他的較小規模的子問題是什麼?正如前面別人家的分析所知,我們必須先知道較短長度s.size()-2的子串是否是迴文,那麼s.size()的源字串就可以被判斷出來

接著嘮嗑,為了判斷“較短長度s.size()-1的子串是否為迴文”,我們必須先知道較短長度s.size()-3的子串是否是迴文,那麼s.size()-1的源字串就可以被判斷出來

接著嘮嗑,為了判斷“較短長度s.size()-2的子串是否為迴文”,我們必須先知道較短長度s.size()-4的子串是否是迴文,那麼s.size()-2的源字串就可以被判斷出來

...............

顯然迴圈的最外層就是當前要判斷的子串的長度,由小到大。

迴圈的最內層就是遍歷當前指定子串長度的起點和終點,

而最長的迴文在我們遍歷的過程更新即可!



例子2

跳臺階問題

LeetCode解題報告 70. Climbing Stairs

定義子問題:令vec[i]表示跳到第i步可行的不同方式數目

接著尋找當前子問題vec[i]與前面子問題的關係,

如果是用兩步跳過來的(跳到第i步)則vec[i]=vec[i-2],因為這兩步已經確定了,那麼只有vec[i-2]中可能

如果是用一步跳過來的(跳到第i步)則vec[i]=vec[i-1],因為這一步已經確定了,那麼只有vec[i-1]中可能

時間複雜度:O(n)

空間複雜度:O(n)


例子3

經典的股票買賣問題

<LeetCode OJ> 123/188 Best Time to Buy and Sell Stock (III / IV)

以下是123題的動態規劃分析(同樣適用於188題):

本體和揹包問題一樣,有兩個變數,第i天,交易第j次
定義子問題:f[i][j]表示前i天交易j次能得到的最大利潤 
1,

對於第i天的物品有兩種選擇情況:交易(買或者賣)或者不做任何交易
1)如果不做任何交易:
顯然,此時的最大利潤還是前一天的最大利潤f[j][i] =f[i-1][j] 
2)如果交易: 
為了能在這一天獲得最大利潤如果執行交易顯然只能賣股票(不能買),也就是說只能加上當前price[i]: 
那麼在加上此值price[i]之前的臨時利潤必須是最大的(可以反正法證明),我們稱之為最大臨時利潤maxtmp,

即如果交易,f[j][i] = prices[i] +maxtmp;

綜上兩種情況,f[j][i] = max(f[j][i-1], prices[i] +maxtmp);


繼續求取maxtmp,
對於最大臨時值maxtmp其實也是動態規劃過程
顯然遍歷陣列時求出最大,此時只有兩種情況:每一次可以買,也可以不買
a)若不買:顯然還是以前的maxtmp,即不變 
b)若買:為了能最大顯然是第j-1次的利潤減去,f[i][j - 1] - price[i]

綜上兩種情況,maxtmp=max(maxtmp,f[i][j - 1] - price[i])

更具體的分析為:
那麼假設第j次交易的買進股票是在第z天(實際上在那一天並不重要,我們只是要一個最大的臨時值maxtmp即可) ,其中0<z<i,,那麼必定有j-1次交易完成在z天以內,所以這一次交易的利潤就是price[i] - price[z]
那麼這種情況下的最大利潤f[i][j] 就是 f[z][j-1] + price[i]-price[z] 
那麼當我們遍歷到第i天,即在每次加上(遍歷到)price[i]這個已知值時,先求出price[i]之前的f[z][j-1]-price[z]這個臨時利潤值maxtmp (必須是最大的,其實就是模擬:用手頭已有的利潤減去買股票的支出,所剩的還是最大)


綜上所訴

f[j][i] = max(f[j][i-1], prices[i] +maxtmp);

maxtmp=max(maxtmp,f[i][j - 1] - price[i]);


例子4

楊輝三角問題

<LeetCode OJ> 118./119. Pascal's Triangle(I / II)

以下是118的分析,同樣適用於119.

定義子問題:result[i][j]為三角形i,j位置的值

1,初始化邊界:

for(int i=0;i<numRows;i++)//第一列 全為1
result[i][0]=1; 
for(int i=0;i<numRows;i++)//三角形對角線 全為1
result[i][i]=1; 

2,在初始化基礎上的遞推過程

比較顯然從第二行往下遞推:很容易看出result[i][j]=result[i-1][j]+result[i-1][j-1];



注:本博文為EbowTang原創,後續可能繼續更新本文。如果轉載,請務必複製本條資訊!

原文地址:http://blog.csdn.net/ebowtang/article/details/50791500

原作者部落格:http://blog.csdn.net/ebowtang

本部落格LeetCode題解索引:http://blog.csdn.net/ebowtang/article/details/50668895

相關文章