動態規划入門
動態規划是一種策略,之前也寫過好幾篇入門的文章,但都覺得不太深刻,最近做了不少背包dp
覺得又有了些新的體會,想整理一下。
動態規划是一種多階段決策策略,什么是多階段,就是原問題被划分成了若干個子問題,這些子問題
的類型與原問題類似,只是規模更小,對於每個子問題的決策叫做多階段決策。
動態規划滿足最優子結構和無后效性,什么是最優子結構,動態規划的決策一般都依賴於以前已經決策
好的子問題,這些子問題已經保證了最優解叫做最優子結構,總而保證了這次決策的准確性,無后效性
指的是,這個子問題的決策與后面的子問題無關系。
然后就是動態規划最重要最有思維量的兩個東西——狀態和轉移。
狀態就是每個子問題的條件,我們不仿把動態規划看作一個填表的過程,試想一下,這個題目中有什么
條件會影響我們的決策,狀態的設計要求全面描述和簡潔。
轉移是我認為動態規划中最難的東西,就是設計一個方程,后面的狀態怎么通過前面的最優子結構算出
自己的值。
如果上面比較混亂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 }