蒟蒻亂寫一通關於動態規劃幾類問題的筆記,可能會有錯誤之處,歡迎指正。html
一. 01揹包問題算法
關於這個問題,我以前已經寫了不太全面的(比較扯淡的)筆記,就不復述了。數組
傳送門:揹包問題學習筆記函數
補充一下除了01揹包、徹底揹包、多重揹包外,還有一個超大揹包問題值得了解。學習
二. 最長上升子序列問題(LIS)優化
題目連接:洛谷oj AT2827 LIS 推薦題解:動態規劃——最長上升子序列問題spa
題目不贅述了,LIS就是最長上升子序列。簡單來講,就是在一串給定的數列a[n]中取出一些數(未必要連續),讓它們能單調上升,而且這個數列要最長。.net
舉個例子,對於長度爲10的數列「1,9,11,2,10,7,8,9,13,6」,它的LIS就是「1,2,7,8,9,13」,長度爲6。設計
對於這個問題,有兩種算法,複雜度分別爲O(n2)和O(nlogn)。雖然咱們發現O(n2)的算法是沒法AC洛谷的LIS板子題的,可是O(n2)的算法思想仍然有助於咱們理解動態規劃。code
O(n2)的經典算法:
根據動態規劃把大問題拆成小問題,分段求解的思路,咱們聲明一個數組f[maxn],f[i]表示從1到i中,以a[i]結尾的最長上升子序列的長度。初始時f[i]=1,i∈[1,n]。(初始值其實就是這個序列中只有a[i]時的序列長度,顯然爲1)。
能夠寫出狀態轉移方程:f[i]=max{f[j]+1}, j∈[1,i-1]且a[j]<a[i];
怎麼理解這個方程呢?就是說,當咱們已經處理完了f[i-1],須要求f[i]時,只須要遍歷一遍a[1…i-1],找到全部能成爲a[i]前驅的數a[j](即a[i]>a[j]),而後在全部能成爲前驅的a[j]中找到f[j]最大的那個就能夠了。若是還不理解,能夠嘗試直接看代碼。
由於代碼是寫出來便於理解的,我就不寫寄存器內聯快速讀入之類花裏胡哨的東西了嘻嘻嘻。
#include <cstdio> using namespace std; const int maxn=100000; int n,a[maxn+5],f[maxn+5]; int result; int main(){ scanf("%d",&n); for (int i=1;i<=n;i++){ scanf("%d",&a[i]); f[i]=1; } for (int i=1;i<=n;i++) for (int j=1;j<i;j++) if (a[i]>a[j]&&f[i]<f[j]+1) f[i]=f[j]+1; for (int i=1;i<=n;i++) if (result<f[i]) result=f[i]; printf("%d",result); return 0; }
O(nlogn)的優秀算法:
若是想優化上面的算法,基於貪心的思想,咱們很容易想到:當x,y∈[1,i-1]時,若f[x]=f[y],a[x]<a[y],顯然f[i]=f[x]+1比f[i]=f[y]+1更優,更可能獲得答案。
因此在f[x]必定的狀況下,儘可能選擇更小的a[x]。按f[x]=k來分類,咱們須要記錄的當全部等於k的f[x]中,最小的a[x]。咱們聲明一個low[k]來存儲這個最小的a[x]。
這樣說可能會有點亂,簡單說吧,就是聲明一個low[k],存儲在[1,i-1]之間,已知的最長上升子序列長度爲k的最小的a[x]值。(仍是感受比較複雜,將就理解一下吧)
low[k]=min{a[x]},f[x]=k;
能夠概括出low[k]的幾個性質:
①low[x]單調遞減增,即low[1]<low[2]<low[3]<low[4]<……<low[n-1]<low[n];
②隨着處理時間推動,low[x]只會愈來愈小;
若是不能理解,能夠嘗試本身寫個數列模擬看看。
有了這兩個性質,就能夠這樣求解:
聲明當前已求出的最長上升子序列的長度爲len(初始時爲1),當讀入一個新元素x:
①若x>low[len],則直接把x加入到d的末尾,且len+=1;
②不然,在low[x]中二分查找,找到第一個比x小的數low[k],並low[k+1]=x,在這裏x<=g[k+1]必定成立。
易證時間複雜度爲O(nlogn)。
代碼中的二分查找我用stl的lower_bound函數代替了,可是不開O2會慢挺多吧……手寫二分應該會快,蒟蒻我太懶了orz
#include <cstdio> #include <algorithm> using namespace std; const int maxn=100000; int n,len=1; int a[maxn+5],low[maxn+5]; int main(){ scanf("%d",&n); for (int i=1;i<=n;i++) scanf("%d",&a[i]); low[1]=a[1]; for (int i=2,j=0;i<=n;i++){ if (low[len]<a[i]) j=++len; else j=lower_bound(low+1,low+len+1,a[i])-(low+1)+1; //這裏用stl裏的lower_bound代替手寫的二分查詢 low[j]=a[i]; } printf("%d",len); }
三. 最長公共子序列問題(LCS)
題目連接:洛谷oj P1439 【模板】最長公共子序列 推薦題解:《挑戰程序設計競賽(第二版)》2.3
注意,此次是最長公共子序列(LCS)。LCS就是指給定兩個數列,兩個數列中最長的公共子序列(哇我在說什麼廢話)。
舉個例子好了,好比下面兩個長度分別爲6的子序列:
1 4 9 10 2 6
2 1 10 2 13 6
上面兩個子序列,它們的LCS就是長度爲4的序列: 1 10 2 6 。和LIS同樣,子序列是不須要連續的。
爲了解決這個問題,咱們能夠嘗試這樣思考:
首先,記給定的兩個序列爲s和t,依舊是根據動態規劃分段求解的思想。定義f[i][j]爲序列 s1…si 和序列 t1…tj 對應的LCS的長度。
那麼f[i+1][j+1]有三種狀況:
① si+1=ti+1時,在序列 s1…si 和序列 t1…tj 對應的LCS後面追加si+1(si+1=ti+1);
② 繼承序列 s1…si 和序列 t1…tj+1 對應的LCS;
③ 繼承序列 s1…si+1 和序列 t1…tj 對應的LCS;
f[i][j]爲上面三種狀況中最大的一個。因此能夠寫出遞推式:
這個遞推式能夠在O(n2)的時間內被計算出來,f[n][n]是LCS的長度。