線性DP


很多問題往往會給出一個序列或者一個數表,讓你對其進行划分,或者選出其中的某個最優子集。這一類問題往往適合使用線性DP。
線性DP是一種非常常見的DP。它往往以狀態內的其中一個維度划分階段。接下來,我將給出幾個非常重要的轉移方程。

最長上升(下降)子序列LIS

已知一個序列\(A_i\)。現在我希望從這個序列中從左往右選出若干個元素,使得這些元素組成的子序列元素大小單調遞增。求這樣序列的最大長度。

我們嘗試設計狀態表示這個最大高度。不難發現,只要\(A_i < A_j,i<j\)\(A_j\)就可以和\(A_i\)合並。這個過程和\(A_i\)以前的元素沒有直接的關系。
於是我們嘗試設\(F(i)\)為以\(A_i\)為結尾的,從\(A_1 \sim A_i\)中選出的LIS。不難發現這樣一個轉移關系:

\[ F(i) = \max_{j < i, A_j < A_i}\{F(j)\} + 1 \]

也就是說,我們以前\(i\)個序列划分階段,如果有\(A_j < A_i, j < i\),那么答案就可以從\(F(j)\)轉移到\(F(i)\)
初始值\(F(1) = 1\)

上面這個做法的時間復雜度為\(O(N^2)\),但我們可以通過以下兩種方式做到\(O(N \log N)\)

樹狀數組維護最大值
樹狀數組不能維護區間最大值,但可以維護前綴和后綴最大值。一個狀態\(F(i)\)的決策集合為\(\{j\mid j < i, A_j < A_i\}\)。我們可以在\(A_i\)的值域上建立樹狀數組,保存結尾在\([0,A_i]\)范圍內的最長上升子序列長度。由於我們每次按輸入順序查詢,並更新之,我們自動滿足了\(i < j\)的條件。

貪心+二分
並不能算是標准做法,但是非常巧妙。它並沒有直接設狀態表示最長上升子序列的長度,而是設\(F(i)\)表示當最長上升子序列的長度為\(i\)時,這個子序列末尾的最小值。
有一個比較顯然的貪心策略:當兩個上升子序列長度一樣時,我們應該保留結尾較小者,因為它更有可能接納更長的上升組序列。
核心代碼:

inline int bs(int num)
{
    int l = 0, r = maxlen + 1;
    while(l < r)
    {
	int mid = (l + r) >> 1;
	if(F[mid] > num)
	    r = mid;
	else
	    l = mid + 1;
    }
    return l;
}
int main()
{
    N = qr(1);
    RP(i, 1, N) A[i] = qr(1);

    RP(i, 1, N)
    {
	int pos = bs(A[i]);
	if(pos > maxlen)
	{
	    maxlen = pos;
	    F[pos] = A[i];
	}
	else
	    F[pos] = A[i];
    }
    printf("%d", maxlen);
    return 0;
}

可以看出,盡管DP的速度相較搜索而言相當快了,但也可以通過其他算法進行更深層的優化。

最長公共子序列LCS

給定兩個序列\(A_i\)\(B_i\),現在我們要求找出一個序列\(C\),使得\(C\)既是\(A\)的子序列,又是\(B\)的子序列。請你最大化序列\(C\)長度。

從上一題來看,一個比較直觀的想法是設\(F(i,j)\)表示以\(A_i\)結尾,以\(B_j\)結尾的最長公共子序列。當\(A_i \neq B_j\)時,直接令\(F(i,j)=0\)。當\(A_i = B_j\)時,有轉移方程 \(F(i,j) = \max_{k < i, l < j, A_k = A_i, B_l = B_j}\{F(k,l)\}+1\)

這個方程有兩個比較明顯的問題。首先,\(F(i,j)\)這個狀態是不是過度冗余?很大一部分的\(F(i,j)=0\),這樣會浪費大量的空間。其次,時間復雜度過高,達到了\(O(N^4)\)。如果用鏈表也許可以減少時間,但還是減少不了多少。

考慮這樣一種做法:設\(F(i,j)\)表示前綴序列\(A_{1\cdots i}\)\(B_{1\cdots j}\)的最長個公共子序列之長(此時這個子序列不一定包含\(A_i,B_j\)!)。此時的\(i,j\)可以看作是兩個掃描數組的指針。

\(i,j\)想向后擴展時,有以下情況:

  • \(A_{i+1}=B_{j+1}\)。此時\(i,j\)均往后跳一位,並讓序列的長度\(+1\)
  • \(A_{i+1}\neq B_{j+1}\)。此時指針是不能同時跳的。根據DFS的思想,我們會作如下嘗試:
    • 嘗試讓\(i\)往后跳一次,搜索\((i+1,j)\)這個狀態
    • 嘗試讓\(j\)往后跳一次,搜索\((i,j+1)\)這個狀態
    • 返回兩次嘗試的最大值

綜上,當\(A_{i+1}=B_{i+1}\)時,\(F(i,j)\)可以直接轉移到\(F(i+1,j+1)\),並增加一個貢獻。反之,\(F(i,j)\)應該轉移到\(F(i,j+1)\)或者\(F(i+1,j)\),並取其中的最大值。

把上面的每個方程反過來寫,寫成被轉移狀態關於轉移狀態的表達式,把\(i+1,i\)改成\(i,i-1\),就可以得到轉移方程:

\[ F(i,j) = \begin{cases} F(i-1,j-1)+1 & A_i = B_j\\ \max\{F(i-1,j),F(i,j-1)\} & A_i \neq B_j \end{cases} \]

另外,這道題的階段划分依據叫做“已經處理的前綴長度”。這里並沒有直接指明到底是哪一個前綴,因此任意選擇一個序列即可。

最長公共上升子序列LCIS

LIS和LCS的結合。求序列\(A_i,B_i\)最長的,單調遞增的LCS。

注意到第一題的狀態保證了“取結尾”,而第二題卻沒有。為了降低時間復雜度,這里我們應該采用“半保留”的設計方法。
\(F(i,j)\)表示掃描到前綴子序列\(A_{1\cdots i}\),以\(B_j\)結尾的LCIS。

\(i\)指針向后移一位時,前綴子串會多出來一個新的數\(A_{i+1}\)。對於這個數,我們有兩種方案:

  • 不嘗試匹配這個數。此時\(j\)不動,直接把答案轉移給\(F(i+1,j)\)
  • 嘗試匹配這個數。此時\(j\)往后跳,找到一個位置\(k\),使得\(B_k = A_{i+1}, B_j < B_k\)。嘗試更新這個狀態\(F(i+1,k)\)。注意到可能有多個數匹配到這個位置\(k\),因此不能直接賦值。

把這兩種方案寫成填表法的形式,把上面的\(j\)分別替換成\(j-1\)\(k\),就得到:

\[F(i,j) = \begin{cases} F(i-1,j) & A_i \neq B_j\\ \max_{k < j, B_k < B_j}\{F(i-1,k)\} + 1 & A_i = B_j \end{cases} \]

這個轉移的時間復雜度是\(O(N^3)\)的,其中這里只能以\(A_i\)的前綴子序列長度划分階段。但它還有優化的空間。

注意到在任意時刻,如果我們需要\(O(N)\)枚舉\(F(i,j)\)的決策集合,那么必然有\(A_i = B_j\)。因此,當\(A_i = B_j\)時,轉移方程還可以寫成這個樣子:

\[ F(i,j) = \max_{k < j, B_k < A_i}\{F(i-1,k)\}+1 \]

在每個階段,\(A_i\)的值都是固定的;在當前階段,對決策集合的限制條件只有\(k < j\)。因此,當前\(F(i,j)\)計算完之后,\(j < j' = j+\Delta j\),那么\(F(i,j)\)就會立刻被加到\(F(i,j')\)的決策集合中!
這就是時間復雜度過大的根源。我們花費了額外\(O(N^2)\)的時間來重復掃描這個決策集合,而這個集合實際上是“開源”的,可以供當前階段的所有狀態使用。
因此,我們只需要用一個變量\(F_m\)來維護\(\max_{B_j < A_i}\{F(i,j)\}\)。每當計算完一個狀態\(F(i,j)\),我們就可以拿它來更新\(F_m\)。這樣時間復雜度就降為了\(O(N^2)\)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM