1. 程式人生 > 實用技巧 >動態規劃之LIS與LCS

動態規劃之LIS與LCS

$$ 動態規劃之 \rm LCS 與 LIS$$

仙人長曰:我精通打牌

\(\rm (~Ⅰ~) LCS和LIS\)

\(\qquad\rm LIS:\)最長上升/下降/不降/不升序列
\(\quad\qquad\)方法\((1)\mathcal O(n^2)\)

\(\quad\qquad\quad f_i\)表示第 \(i\) 個元素結尾的 \(\rm LIS\) 長度,則 \(f_i=max(f_i,f_j+1),j< i~\&\&~a_j< a_i\) 。列舉 \(i,j\) 即可。

    for(int i=1;i<=n;++i){
        for(int j=1;j<i;++j){
            if(a[j]<a[i])f[i]=max(f[i],f[j]+1);
        }
    }
\(\quad\qquad\)方法\((2)\mathcal O(nlogn)\)

\(\quad\qquad\quad\)考慮用資料結構優化方法 \((1)\),由於是依次插入序列中的元素,所以當前時刻資料結構中所有的值都是 \(i\) 及之前的元素。那麼我們只需要查詢 \([1,a_i-1]\)\(f_{max}\) 來更新答案,即我們只需要一個支援單點修改和維護字首區間最大值的資料結構。線段樹是一個很好的選擇。

    for(int i=1;i<=n;++i){
        f[i]=query(1,1,n,1,a[i]-1)+1;
        update(1,1,n,a[i],f[i]);
    }
\(\quad\qquad\)方法\((3)\mathcal O(nlogn)\)(這個很妙)

\(\quad\qquad\quad\)先把做法擺出來:維護一個單調棧,對於每次新新增的元素 \(a_i\) ,如果 \(a_i>stk_{top}\) ,就直接把 \(a_i\) 丟到棧裡去;否則從棧中 \(lower\)_\(bound\) 出第一個大於等於 \(a_i\) 的數,把它用 \(a_i\) 替換掉。
\(\quad\qquad\quad\)這個做法乍一看很玄學,證明如下:
\(\quad\qquad\quad\)\(f_i\) 表示以權值 \(i\) 結尾的答案數量, \(g_i\)

表示 \(f_{[1,i-1]}\) 的最大值,\(\Delta g\)\(g_i\) 的差分陣列。那麼顯然 \(f_i=g_i+1\) 。我們每次插入 \(a_i\) ,那麼 \(f_{a_i}\) 更新為 \(g_{a_i}+1\) 。然後考慮這樣對於後面的 \(g\) 的影響。由於這個元素是後插入的,所以一定不影響到之前插入的元素的答案 \(f\) ,同時 \(g\) 也一定只改變某一段空的區間。
\(\quad\qquad\quad\)舉個栗子: \(1,4,2,4,5,6\)
\(\quad\qquad\quad\)先插入 \(1\)

陣列 1 2 3 4 5 6
\(f\) 1 0 0 0 0 0
\(g\) 0 1 1 1 1 1
\(\Delta g\) 0 1 0 0 0 0

\(\quad\qquad\quad\)再插入 \(4\)

陣列 1 2 3 4 5 6
\(f\) 1 0 0 2 0 0
\(g\) 0 1 1 1 2 2
\(\Delta g\) 0 1 0 0 1 0

\(\quad\qquad\quad\)再插入 \(2\)

陣列 1 2 3 4 5 6
\(f\) 1 2 0 2 0 0
\(g\) 0 1 2 2 2 2
\(\Delta g\) 0 1 1 0 0 0

\(\quad\qquad\quad\)剩下的自己手膜一下吧。
\(\quad\qquad\quad\)從栗子中我們可以看出,每次我們修改 \(g\) 的時候,只需要從 \(a_i+1\) ,到 \(a_i+1\) 往後及 \(a_i+1\) 本身這樣一個範圍中,第一個非零的數(設為 \(pos\) )之前的所有 \(g\) 值都 \(+1\) ,也就相當於在 \(\Delta g\)\(\Delta g_i++,\Delta g_{pos}--\)
\(\quad\qquad\quad\)回到做法中,我們維護一個單調棧相當於儲存了所有有權值的 \(f\) ,然後每次是在 \(a_i\) ~ \(a_{pos}\) 進行修改,將 \(f_{a_i}\) 賦予權值然後 \(f_{pos}\) 的答案就可以忽略不計了(因為我們實際上維護的是 \(\Delta g\) ,而 \(a_{pos}\)\(\Delta g\) 的貢獻已經被 \(a_i\) 給覆蓋掉了)。最後我們的答案,其實就是 \(\Delta g\) 的和,也就是棧的長度。(因為每一次如果是入棧操作則 \(len++\)\(\Delta g\) 的和也 \(++\);如果是替換操作則 \(len\) 不變, \(\Delta g\) 的和先 \(+1\)\(-1\) 也不變)

    d[1]=a[1];
    for(int i=2;i<=n;++i){
        if(d[len]<a[i])d[++len]=a[i];
        else *lower_bound(d+1,d+1+len,a[i])=a[i];
    }
\(\quad\qquad(4)\)拓展:求具體 \(\rm LIS\) 序列。

\(\quad\qquad\quad\)這個應該只能用方法 \((2)\) 來做,只需要記錄一下從哪裡轉移過來的即可。


\(\qquad\rm LCS:\)最長公共子序列
\(\quad\qquad(1)\mathcal O(nm)\)

\(\quad\qquad\quad\)考慮 \(dp\)

\[f_i= \begin{cases} max(f_{i,j},f_{i-1,j-1}+1) & \text{$a_i=b_j$}\\ max(f_{i,j-1},f_{i-1,j})& \text{a_i $\ne$ b_j} \end{cases}\]

\(\quad\qquad(2)\mathcal O(nlogn)\)

\(\quad\qquad\quad\)我們可以發現,如果兩個序列一個是升序,另外一個亂序時,求兩個序列的 \(\rm LCS\)其實就是在求亂序序列的 \(\rm LIS\) (因為有一個序列是升序的,所以一定存在亂序序列的最長上升子序列與升序序列的序列有最長公共子序列)。
\(\quad\qquad\quad\)所以我們在一開始輸入 \(a\) 序列的時候就將它序列中的元素逐一編號,然後再在 \(b\) 序列給對應的元素編上在 \(a\) 序列中的編號,也就是 \(b\) 序列中元素在 \(a\) 序列中的位置。
\(\quad\qquad\quad\)比如說

        a: 1 5 4 3 2
        b: 5 3 1 2 4

\(\quad\qquad\quad\)編號之後就是

        a: 1 2 3 4 5
        b: 2 4 1 5 3

\(\quad\qquad\quad\)然後對 \(b\)\(\rm LIS\) 即可。
\(\quad\qquad\quad\)但是需要注意的是:
\(\quad\qquad\quad\)首先,序列元素不可重。因為如果重複的話,你標完好以後的 \(a\) 陣列不能保證升序。比如說 \(1,2,1,3,3\) ,實際上標完好以後的序列是 \(0,2,1,0,5\) ,某些位置被覆蓋掉了。
\(\quad\qquad\quad\)其次,如果兩個序列有不同元素,處理時要除去不同元素。顯然除去不會對答案造成影響,不除去的話會出錯(自己手膜幾組樣例就知道了)


\(\qquad\rm LCIS\)

\(\quad\qquad\quad\)\(f_{i,j}\) 表示 \(a_1\) ~ \(a_i\)\(b_1\) ~ \(b_j\) 並以 \(b_j\) 結尾的最長公共上升子序列。

\[f_{i,j} \begin{cases} f_{i-1,j} & a_i \ne a_j\\ max(f_{i-1,k})+1,b_k < b_j 且 1 \le k < j & a_i = a_j \end{cases} \]

\(\quad\qquad\quad\)這樣做的複雜度是 \(\mathcal O(n^3)\) 的。
\(\quad\qquad\quad\)但是我們可以通過維護 \(k\) 來實現優化。
\(\quad\qquad\quad\)先丟程式碼:

            for(int i=1,k=0;i<=n;++i,k=0){
                for(int j=1;j<=m;++j){
                    if(a[i]==b[j])f[i][j]=f[i-1][k]+1,pre[j]=k;//pre記錄前一位,用於記錄具體LCIS
                    else f[i][j]=f[i-1][j];
                    if(a[i]>b[j]&&f[i-1][j]>f[i-1][t])k=j;
                }
            }

\(\quad\qquad\quad\)前面兩句好理解,關鍵是第三句。對於我們當前列舉到的 \(a_i\) ,我們要找的是一個滿足 \(a_i=b_g\)\(g\) ,然後再在 \(g\) 的基礎上找一個 \(k\) 滿足 \(b_k < b_g\) ,那麼我們可以更換一下列舉的 \(g,k\) 順序。顯然,我們將要找到的 \(b_g=a_i\) ,然後再去找 \(b_k< b_g\) ,其實就等效於維護一個 \(b_k < a_i\) ,因為反正最後要找到的 \(b_g=a_i\) 。同時又要記錄 \(f\) 最大,因此就是上面那個式子了。
\(\quad\qquad\quad\)若是要求具體 \(\rm LCIS\) 就記錄一下 \(pre\) 就可以了。