最長上升子序列

算法簡述

最長上升子序列(Longest Increasing Subsequence, 簡稱LIS)是dp中比較經典的一個算法模型, 它有一種樸素的算法O(n^2)和一種優化版的算法(nlogn)實現, 通過它, 我們可以進一步了解dp的思想.

題目鏈接

pku-2533 Longest Ordered Subsequence

題意

給定一個長度為1000以內的數組,每個元素范圍都在[0,10000]的整數,求這個數組的LIS.

解法

記數組為a[0...n-1],算法很直接,具體如下:

  1. 狀態定義:
    dp[i]代表以第i項為結尾的LIS的長度.
  2. 狀態轉移:
    dp[i] = max(dp[i], max(dp[j]) + 1) if j < i and a[j] < a[i]
  3. 狀態初始化:
    dp[i]=1
  4. 時間復雜度:
    狀態數為n, 每次轉移復雜度是O(n), 所以算法總復雜度是O(n^2)

核心代碼:

for(i = 0; i < n; ++i) {
    for(j = 0; j < i; ++j) {
        if(a[j] < a[i]) {
            dp[i] = max(dp[i], dp[j] + 1);
        }
    }
}

完整代碼

算法優化

再來看一道題:

題目鏈接:

hdu-1950 Bridging signals

題意:

這題是一個經典的布線問題, 如下圖所示: 左右各有n(n<40000)個點, 原本左邊的每個點分別跟右邊的一個點相連(不會有兩個左邊的點連同一個右邊的點), 要求我們拆除一部分線, 保留盡可能多的線, 使得剩下的線兩兩不能有交點.

解法:

記左邊的兩個點為i, j且i < j, 與之相連的右邊的點分別為a[i], a[j], 則兩條線不相交的充要條件就是: a[i]<a[j], 于是這個問題轉化成了經典的LIS.
但是這題與上題最大的差別就在于: 數組的長度太大了, 由1000變到了40000, 于是O(n^2)復雜度基本只有超時的命運, 所以我們必須想辦法優化.

更優的算法

下面介紹一種O(nlogn)的LIS算法:

  1. 記數組為a[0...n-1];
  2. 狀態定義:
    dp[i]代表LIS的第i項最小值, dpLen代表當前dp數組的長度;
  3. 狀態轉移:
    dp初始為空數組, 我們按a數組元素的下標順序進行掃描, 假設現在掃描到a[i], 先找到dp數組中第一項大于或等于a[i]的元素, 記為dp[j]; 將dp[j]更新成a[i]即可; 如果dp數組中沒有元素比a[i]大的話, 那么直接將a[i]插入到dp數組的尾部,再更新dp數組長度;
  4. 整個數組的LIS結果就是dpLen.
  5. 需要注意的是, 雖然dp數組最終長度就是LIS, 但是里邊的元素并不是真正的子序列, 如果要求輸出這個序列, 加上一些反向追蹤變量就能得到了. 但是如何求LIS的數量呢?
  6. 剛開始dp數組為空,顯然是單調遞增數組, 而后面的每一步替換或者尾部插入執行都不影響其單調遞增的特性, 所以每次定位到dp[j]可以用二分法, 復雜度是 O(logn)
  7. 整體算法復雜度:
    狀態轉移次數為n, 每次狀態轉移代碼都是logn, 所以總復雜度為O(nlogn).

算法步驟示例:

假設a = [4, 2, 6, 3, 1, 5], 初始dp=[], 具體算法運行步驟如下:

  1. a[0]=4 => dp=[4];
  2. a[1]=2 => dp=[2];
  3. a[2]=6 => dp=[2, 6];
  4. a[3]=3 => dp=[2, 3];
  5. a[4]=1 => dp=[1, 3];
  6. a[5]=1 => dp=[1, 3, 5];
    所以這個a數組的LIS就是len(dp)=3. 從運行步驟里可以看出, 如果一個數很小, 可以作為LIS的頭部或者中部, 讓后面的數字更容易接到它后面, 以此增大LIS長度; 而一個數非常大, 則可以很容易接到LIS的尾部, 也一樣能增大LIS長度; 所以讓它們找準自己的定位還是非常重要的.

核心代碼:

dpLen = 0;
for (i = 0; i < n; ++i) {
    int idx = lower_bound(dp, dp + dpLen, a[i]) - dp;
    dp[idx] = a[i];

    if (idx + 1 > dpLen) {
        dpLen = idx + 1;
    }
}

完整代碼
ps: Java里沒有提供類似lower_bound或者upper_bound
之類的方法, 還是挺遺憾的, 所以這題代碼就用C++了.

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 背景 一年多以前我在知乎上答了有關LeetCode的問題, 分享了一些自己做題目的經驗。 張土汪:刷leetcod...
    土汪閱讀 12,776評論 0 33
  • 假設存在一個序列d[1..9] = 2 1 5 3 6 4 8 9 7,可以看出來它的LIS長度為5。n下面一步一...
    Gitfan閱讀 380評論 0 0
  • 動態規劃(Dynamic Programming) 本文包括: 動態規劃定義 狀態轉移方程 動態規劃算法步驟 最長...
    廖少少閱讀 3,338評論 0 18
  • 題目 給定一個整數序列,找到最長上升子序列(LIS),返回LIS的長度。說明最長上升子序列的定義:最長上升子序列問...
    六尺帳篷閱讀 395評論 0 1
  • 女孩 我想告訴你 你要加油。也許 你現在經歷著一些非常難受 痛苦的事。但你別忘了 終會有一天 著一些都會過去 成為...
    夏天_ysumvonnemer閱讀 272評論 2 0