動態規劃幾類例題的筆記

  蒟蒻亂寫一通關於動態規劃幾類問題的筆記,可能會有錯誤之處,歡迎指正。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…s和序列 t1…t對應的LCS的長度。

  那麼f[i+1][j+1]有三種狀況:

    ① si+1=ti+1時,在序列 s1…s和序列 t1…t對應的LCS後面追加si+1(si+1=ti+1);

    ② 繼承序列 s1…s和序列 t1…tj+1 對應的LCS;

    ③ 繼承序列 s1…si+1 和序列 t1…t對應的LCS;

  f[i][j]爲上面三種狀況中最大的一個。因此能夠寫出遞推式:

    

  這個遞推式能夠在O(n2)的時間內被計算出來,f[n][n]是LCS的長度。

相關文章
相關標籤/搜索