動態規劃入門算法
動態規劃是一種策略,以前也寫過好幾篇入門的文章,但都以爲不太深入,最近作了很多揹包dp數組
以爲又有了些新的體會,想整理一下。數據結構
動態規劃是一種多階段決策策略,什麼是多階段,就是原問題被劃分紅了若干個子問題,這些子問題優化
的類型與原問題相似,只是規模更小,對於每一個子問題的決策叫作多階段決策。spa
動態規劃知足最優子結構和無後效性,什麼是最優子結構,動態規劃的決策通常都依賴於之前已經決策設計
好的子問題,這些子問題已經保證了最優解叫作最優子結構,總而保證了此次決策的準確性,無後效性code
指的是,這個子問題的決策與後面的子問題無關係。blog
而後就是動態規劃最重要最有思惟量的兩個東西——狀態和轉移。入門
狀態就是每一個子問題的條件,咱們不仿把動態規劃看做一個填表的過程,試想一下,這個題目中有什麼class
條件會影響咱們的決策,狀態的設計要求全面描述和簡潔。
轉移是我認爲動態規劃中最難的東西,就是設計一個方程,後面的狀態怎麼經過前面的最優子結構算出
本身的值。
若是上面比較混亂qwq,你或許能夠看看下面的板正版的
一,概念篇
1,動態規劃:經過計算出小問題的最優解,能夠推出大問題的最優解,從而能夠推出更大問題的最優解,最小問題便是邊界狀況。
2,子問題(小問題):子問題是一個與原問題有着相似的結構,但規模比原問題小的問題。
3,最優子結構:動態規劃的問題通常是求解全局最優解,而全局最優解是由局部的最有解一步一步推出,局部的最優解稱爲最優子結構。
4,動態規劃的基本思想:將待求解的問題劃分爲若干個階段(子問題),按順序求解子問題,子問題的求解爲更大子問題的求解提供信息,因爲動態規劃解決的問題多數有重疊子問題這個特色,爲減小重複計算,對每個子問題只解一次。
動態規劃的核心思想也有減小冗餘,對於會發生重疊的子問題只計算一次,去除冗餘。
5,狀態表示和最優化值。
狀態表示是對當前子問題的解的局面的(條件)一種全面的描述。
最優化值是該狀態表示下的最優化值(方案值),咱們最終能經過其直接或間接獲得答案。
6,狀態的設計
具備最優化總結構,可以全面描述某一個局面,儘可能簡潔。
設計狀態的關鍵是充分描述,儘可能簡潔。
7,動態規劃的精髓——狀態轉移:經過已知的較小問題的最優值得出較大問題的最有值的過程。狀態的轉移須要知足要考慮到全部的可能性。狀態轉移的實質是一個DAG,能夠把狀態抽象成點,轉移抽象成邊,轉移就是從子問題指向當前狀態。
8,無後效性:由於動態規劃的轉移過程是一個DAG,因此保證了當前狀態僅由以前的子問題轉移而來,而與後面的狀態沒有什麼關係。
9,動態規劃的時間複雜度估計
O=狀態數*狀態轉移的複雜度
動態規劃通常由記憶化搜索或者數組遞推實現。
動態規劃經常使用於解決計數問題(求方案數的問題)和最有值問題(最大價值,最小花費)
入門例題不想過多的去講,就闡述一道最經典的數字三角形來模擬一下動態規劃的思路
觀察下面的數字金字塔。
寫一個程序來查找從最高點到底部任意處結束的路徑,使路徑通過數字的和最大。每一步能夠走到左下方的點也能夠到達右下方的點。
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
顯然這是一個最有值問題,首先適用於動態規劃求解。
狀態:這個題只有位置須要記錄,位置會影響咱們的決策,很顯然。
狀態轉移:顯然要從下面到頂部的最大值,確定要求從這個點左下方來的路徑與右下方來的路徑哪一個和大要哪一個,下面的每一個點
都是經過這個策略求出的,因此知足最優子結構。
線性動態規劃經典——最長上升子序列(LIS)
顧名思義,最長上升子序列就整個序列知足單調遞增性質的子序列中長度最長的一個。
好比 序列 5,3,2,6,7,
其中上升的子序列有
5; 3; 2; 6; 7; 2,6;2,6,7; 6,7;3,6;3,7; 3,6,7;5,6;5,6,7;5,7;
顯然其中最長的子序列是2,6,7或3,6,7或5,6,7。最長上升子序列的長度爲3。
如何用動態規劃的方法求解最長上升子序列的長度?
方法很簡單,咱們設一個數組dp[i]表示以a[i]爲結尾的最長上升自序列,最後全部dp[i]中最大的那個就是這個序列的LIS。
求解dp數組的方法:
顯然,咱們爲了讓序列的長度更長,因此必定要把a[i]接在它能接在的dp[j]最大的a[j]後面,所以咱們就獲得了求解dp數組的狀態轉移方程:
dp[i]=max{ dp[j] | a[j]<a[i] && j<i } +1 ;
所以咱們就獲得了一個O(n^2 )的作法:
1 for(int i=1;i<=n;i++) 2 3 { 4 5 dp[i]=1; 6 7 for(int j=1;j<i;j++) 8 9 if(a[i]>a[j]&&dp[j]+1>dp[i]) dp[i]=dp[j]+1; }
若數據範圍較大,顯然O(n^2 )的作法恐怕不行,那麼咱們是否能夠優化一下這個作法,dalao zhaohx講動態規劃的優化分爲兩種第一種是減小狀態量,、
第二種是加快轉移過程。加快轉移過程又分爲性質優化和數據結構優化。LIS能夠採用性質優化和數據結構優化兩種。
1,性質優化 O(nlogn)
咱們仔細觀察以前的那個狀態轉移方程dp[i]=max{ dp[j] | a[j]<a[i] && j<i } +1 ;
咱們會發現這個狀態轉移方程的做用,它每次尋找的是小於a[i]的a[j]中dp[j]最大的那一個,此時咱們能夠採用一個輔助數組h[k]表示,
咱們設h[k]表示dp[j]==k的全部j當中的最小的a[j],就是說長度爲k的最長上升序列,最後一個元素的最小值是多少,由於最後一個元素越小,
確定後面更容易再加上一個元素了。
所以咱們發現了一個性質,h[k]必定是單調遞增的,當咱們新加入一個元素的時候,若是這個元素比末尾元素大,那麼這個元素就能夠接在末尾
以後即h[++k]=a[i];若是這個元素小於末尾的元素,咱們就要給這個元素找一個位置,位置知足的性質是這個元素a[i]能接在某個h[k]以後,且這
個元素要小於h[k+1],由於這樣後面的元素才更容易接上,這裏咱們運用到了二分查找來修改。
算法流程:
若是這個元素大於d[len],直接讓d[len+1]=a[i],而後len++。這個很好理解,當前最長的長度變成了len+1,並且d數組也添加了一個元素。
若是這個元素等於d[len],那麼能夠保證d[1..len-1]都是小於a[i]的(根據上面的證實),所以這個元素就沒有什麼意義了,直接忽略就好,
由於它沒法接在任何一個元素d後面產生一個更有優點的子序列。
若是這個元素小於d[len],那麼就在d數組中找到第一個大於等於它的元素(這個元素必然存在,至少d[len]就是),把這個元素替換成a[i]便可。
它一定能夠替換掉一個比它大的但在同一h[k]的元素,好比最特殊的狀況它會替換掉h[k]。
1 for(int i=1;i<=n;i++) 2 { 3 if(num[i]>dp[len]) dp[++len]=num[i]; 4 else if(num[i]<dp[len]) 5 { 6 int head=1,tail=len; 7 while(head<=tail) 8 { 9 int mid=(head+tail)>>1; 10 if(dp[mid]>=num[i]) tail=mid; 11 else head=mid+1; 12 } 13 dp[head]=num[i]; 14 } 15 }
2,樹狀數組優化,表示並不會
對相似問題的探討
相似的還有最長不降低子序列,最長降低子序列以及最長上升子序列。
注意最長不降低子序列和最長上升子序列不一樣,最長不降低子序列左右元素能夠相等。
線性動態規劃經典——最長公共子序列
最長公共子序列:子序列與子串不一樣,能夠不連續。
咱們能夠類比最長上升子序列,設dp[x][y]表示第一個串的前x位與第二個串的前y位的最長公共子序列。
考慮三種狀況
1,若是s1[x]不在公共子序列中,那麼dp[x][y]=dp[x-1][y];
2,若是s2[y]不在公共子序列中,那麼dp[x][y]=dp[x][y-1];
3,若是s1[x]==s2[y],dp[x][y]=dp[x-1][y-1]+1;
綜上狀態轉移方程爲三者的最大值。
1 inline void lcs(char *s1,char *s2) 2 { 3 int s1len=strlen(s1+1); 4 int s2len=strlen(s2+1); 5 for(int i=1;i<=s1len;i++) 6 for(int j=1;j<=s2len;j++) 7 { 8 dp[i][j]=max(dp[i-1][j],dp[i][j-1]); 9 if(s1[i]==s2[j]) dp[i][j]=max(dp[i][j],dp[i-1][j-1]+1); 10 } 11 }