【PAT】5. 動態規劃

magic_jiayu發表於2020-12-23

【PAT】5. 動態規劃

動態規劃的遞迴和遞推寫法

  • 如果一個問題可以被分解為若干個子問題,且這些子問題會重複出現,那麼就稱這個問題擁有重疊子問題(Overlapping Subproblems)
  • 如果一個問題的最優解可以由其子問題的最優解有效的構造出來,那麼稱這個問題擁有最優子結構(Optimal Substructure)
  • 一個問題必須擁有重疊子問題和最優子結構,才能使用動態規劃去解決。
  • 狀態的無後效性是指:當前狀態記錄了歷史資訊,一旦當前狀態確定,就不會再改變,且未來的決策只能在已有的一個或若干個狀態的基礎上進行,歷史資訊只能通過已有的狀態去影響未來的決策
  • 必須設計一個無後效性的狀態以及相應的狀態轉移方程,這也是動態規劃的核心

最大連續子序列和

給定一個陣列序列 A 1 , A 2 , . . . , A n A_1,A_2,...,A_n A1,A2,...,An,求 i , j ( 1 ≤ i ≤ j ≤ n ) i,j(1\leq i\leq j\leq n) i,j(1ijn),使得 A i + . . . + A j A_i+...+A_j Ai+...+Aj最大,輸出這個最大和。

  1. 暴力解法( O ( n 3 O(n^3 O(n3)):列舉左端點和右端點(即列舉i,j)需要 O ( n 2 ) O(n^2) O(n2)的複雜度,而計算 A i + . . . + A j A_i+...+A_j Ai+...+Aj需要 O ( n ) O(n) O(n)複雜度
  2. 記錄字首和( O ( n 2 ) O(n^2) O(n2)):預處理 S [ i ] = A 1 , A 2 , . . . , A i S[i]=A_1,A_2,...,A_i S[i]=A1,A2,...,Ai,這樣 A i + . . . + A j = S [ j ] − S [ i − 1 ] A_i+...+A_j=S[j]-S[i-1] Ai+...+Aj=S[j]S[i1]
  3. 動態規劃(O(n)),其實左端點的列舉是沒有必要的。
    • 令狀態dp[i]表示以A[i]作為末尾的連續序列的最大和
    • 狀態轉移方程dp[i] = max{A[i],dp[i-1]+A[i]},邊界dp[0]=A[0];

最長不下降子序列(LIS)

最長不下降子序列(Longest Increasing Sequence, LIS):在一個數字序列中,找到一個最長的子序列(可以不連續),使得這個子序列是不下降(非遞減)的。

令dp[i]表示以A[i]結尾的最長不下降子序列長度,這樣對A[i]來說就會有兩種可能:

  1. 如果存在A[i]之前的元素A[j](j < i),使得A[j]<=A[i]dp[j] + 1 > dp[i](即把A[i]跟在以A[j]結尾的LIS後面,形成一條更長的不下降子序列)
  2. 如果A[i]之前的元素都比A[i]大,那麼A[i]就只好自己形成一條LIS,但是長度為1.

最後以A[i]結尾的LIS長度就是步驟1,2中能形成的最大高度。

  • 狀態轉移方程dp[i]=max{1,dp[j]+1} (j=1,2,...,i-1 && A[j]<A[i])
  • 邊界dp[i]=1(i~[1,n])

最長公共子序列(LCS) 2

最長公共子序列(Longest Common Subsequence, LCS):給定兩個字串(或數字序列)A和B,求一個字串,使得這個字串是A和B的最長公共部分(子序列可以不連續)

令dp[i][j]表示字串A的i號位和字串B的j號位之前的LCS長度(下標從1開始),根據A[i]和B[j]的情況,分為兩種決策:

  1. A[i]==B[j],則字串S與字串B的LCS增加了一位,有dp[i][j] = d[i-1][j-1] + 1
  2. A[i]!=B[j],則字串A的i號位和字串B的j號位之前的LCS無法延長,因此dp[i][j]將會繼承dp[i-1][j]與dp[i][j-1]中的較大值,即有dp[i][j]=max{dp[i-1,j],dp[i][j-1]}
  3. 邊界:dp[i][0] = dp[0][j] = 0

最長迴文子串 3

給出一個字串S,求S的最長迴文子串的長度

令dp[i][j]表示S[i]至S[j]所表示的子串是否是迴文子串,是則為1,不是為0。根據S[i]是否等於S[j],可以把轉移情況分為兩類:

  1. S[i]==S[j],那麼只要S[i+1]至S[j-1]是迴文子串,S[i]至S[j]就是迴文子串,令dp[i][j] = dp[i+1][j-1]
  2. 若S[i]!=S[j],那麼一定不是迴文子串,令d[i][j]=0
  3. 邊界:dp[i][i]=1,dp[i][i+1]=(S[i]=S[i+1])?1:0

注意:如果按照i和j從小到大的順序來列舉子串的兩個端點,然後更新dp[i][j],會無法保證dp[i+1][j-1]已經被計算過,從而無法得到正確的dp[i][j]。

注意到邊界表示的是長度為1和2的子串,且每次轉移時都對子串的長度減了1,不妨考慮按子串的長度和子串的初始位置進行列舉,即可以先列舉子串長度L(L是可以取到整個字串的長度S.len()的),再列舉左端點i,這樣右端點i+L-1也可以直接得到

  • 也可以通過二分+字串hash,複雜度為 O ( n l o g n ) O(nlogn) O(nlogn)
  • 最優秀的是Manacher演算法,複雜度為 O ( n ) O(n) O(n)

數塔DP

將一些數字排成數塔的形狀,第n層有n個數字。現在從第一層走到第n層,每次只能走向下一層連線的兩個數字中的一個,問:最後將路徑上所有數字相加後得到的和最大是多少。

令dp[i][j]表示從第i行第j個數字出發的到達最底層的所有路徑中能得到的最大和。狀態轉移方程為dp[i][j]=max(dp[i+1][j],dp[i+1][j+1]) + f[i][j];由於數塔的最後一層的dp值總是等於元素本身,即邊界為dp[n][j]=f[n][j](1<=n<=j)

DAG最長路

給定一個有向無環圖,怎麼樣求解整個圖的所有路徑中權值之和最大的那條。

  • 令dp[i]表示從i號頂點出發能獲得的最長路徑長度,所有dp[i]的最大值就是整個DAG的最長路徑長度
  • 求解dp陣列:如果從第i號頂點出發能直接到達頂點 j 1 , j 2 , . . . j k j_1,j_2,...j_k j1,j2,...jk,而 d p [ j 1 ] , d p [ j 2 ] , . . . , d p [ j k ] dp[j_1],dp[j_2],...,dp[j_k] dp[j1],dp[j2],...,dp[jk]均已知,那麼有 d p [ i ] = m a x { d p [ j ] + l e n g t h [ i → j ] ∣ ( i , j ) ∈ E } dp[i]=max\left \{ dp[j]+length[i\rightarrow j] |\left ( i,j \right )\in E \right \} dp[i]=max{dp[j]+length[ij](i,j)E},因此我們需要逆拓撲序列的順序來求解dp陣列,或者使用遞迴的方法,不求出逆遞迴陣列也能計算dp陣列
  • 由於由出度為0的頂點出發的最長路徑長度為0,因此邊界為這些頂點的dp值為0,具體實現中對整個dp陣列初始化為0。遞迴求解出度不是0的頂點,遞迴過程中遇到已經計算過的頂點則直接返回對應的dp值。
  • 求解具體最長路徑(類比Dijkstra):開一個int型choice陣列記錄最長路徑上頂點的後繼結點。如果最終可能有多條最長路徑,將choice陣列改為vector型別的陣列即可
  • 如果DAG中有多條路徑,選取字典序最小的那條:只需要讓遍歷i的鄰接點的順序從小到達即可(下面程式碼自動實現了這個功能)
  • 如果令dp[i]表示以i號頂點結尾能獲得的最長路徑長度,只要把求解公式變為 d p [ i ] = m a x { d p [ j ] + l e n g t h [ j → i ] ∣ ( j , i ) ∈ E } dp[i]=max\left \{ dp[j]+length[j\rightarrow i] |\left ( j,i \right )\in E \right \} dp[i]=max{dp[j]+length[ji](j,i)E}(相應的求解順序變為拓撲序),同樣可以得出結果,但不能直接得到字典序最小的方案。因為字典序的大小總是先根據序列中較前的部分來判斷,因此序列中越靠前的頂點,其dp值應當越後計算。
    int DP(int i){  //i為源點
        if(dp[i] > 0){  
            return dp[i];   //dp[i]已計算得到
        }
        for(int j = 0; j < n; j++){//遍歷i的所有邊
            if(G[i][j] != INF){
                int temp = DP(j) + G[i][j]; //單獨計算,防止if中呼叫DP函式兩次
                if(temp > dp[i]){   //可以獲得更長的路徑
                    dp[i] = temp;   //覆蓋dp[i]
                    choice[i] = j;  //i號頂點的後繼結點是j
                }
            }
        }
        return dp[i];   //返回計算完畢的dp[i]
    }
    //呼叫printfPath前需要先得到最大的dp[i],然後將i作為路徑起點傳入
    void printPath(int i){
        printf("%d", i);
        while(choice[i] != -1){ //choice陣列初始化為-1
            i = choice[i];
            pritnf("->%d", i);
        }
    }
    

固定終點,求DAG的最長路徑長度

  • 令dp[i]表示從i號頂點出發到達終點T能獲得的最長路徑長度,狀態轉移方程和上面一樣,但是邊界有很大區別,設定邊界為dp[T]=0,並且初始化dp陣列為一個負的大數,來保證“無法到達終點”的含義的以表達(即-INF);然後設定一個vis陣列表示頂點是否已經被計算,
    int DP(int i){
        if(vis[i]){
            return dp[i];
        }
        vis[i] = true;
        for(int j = 0; j < n; j++){
            if(G[i][j] != INF){
                dp[i] = max(dp[i], DP(j) + G[i][j]);
            }
        }
        return dp[i];
    }
    

揹包問題

多階段動態規劃問題:問題可以描述成若干個有序的階段,且每個階段的狀態只和上一個階段的狀態有關。01揹包問題就是這樣的例子

01揹包問題

有n件物品,每件物品的單件重量為w[i],價值為c[i]。現有一個容量為V的揹包,問如何選取物品放入揹包,使得揹包內物品的總價值最大。其中每種物品都只有一件。

dp[i][v]表示前i件物品恰好裝入容量為v的揹包中所能獲得的最大價值。考慮對第i件物品的選擇策略,有兩種策略:

  1. 不放第i件物品,那麼問題轉化為前i-1件物品恰好裝入容量為v的揹包中所能獲得的最大價值,也即dp[i-1][v]
  2. 放第i件物品,那麼問題轉化為前i-1件物品恰好裝入容量為v-w[i]的揹包中所能獲得的最大價值,即dp[i-1][v-w[i]]+c[i]

因此狀態轉移方程為 d p [ i ] [ v ] = m a x { d p [ i − 1 ] [ v ] , d p [ i − 1 ] [ v − w [ i ] ] + c [ i ] } ( 1 ≤ i ≤ n , w [ i ] ≤ v ≤ V ) dp[i][v]=max\left \{ dp[i-1][v] , dp[i-1][v-w[i]]+c[i] \right \}\left (1\le i\le n,w[i]\le v \le V \right ) dp[i][v]=max{dp[i1][v],dp[i1][vw[i]]+c[i]}(1in,w[i]vV)

邊界為 d p [ 0 ] [ v ] = 0 ( 0 ≤ v ≤ V ) dp[0][v]=0\left (0\le v\le V \right ) dp[0][v]=0(0vV)(即前0件物品放入任何容量為v的揹包中都只能獲得價值0)。

注意到dp[i][v]只與之前的狀態dp[i-1][]有關,所以可以列舉i從1到n,v從0到V,通過邊界來遞推出整個dp陣列。由於dp[i][v]表示的是恰好為v的情況,所以要列舉 d p [ n ] [ v ] ( 0 ≤ v ≤ V ) dp[n][v]\left(0\le v\le V \right ) dp[n][v](0vV),取其最大值才是最後的結果。時間複雜度為 O ( n V ) O(nV) O(nV)

for(int i = 1; i <= n; i++){    //前i件物品
    for(int v = w[i]; v <= V; v++){ //容量至少為w[i],最大為V
        dp[i][v]=max(dp[i-1][v],dp[i-1][v-w[i]]+c[i]);
    }
}

優化空間複雜度:注意到狀態轉移方程中計算dp[i][v]時總是隻需要dp[i-1][v]左側部分的資料,且當計算dp[i+1][]部分時,dp[i-1]的資料又完全又不到了(只需要用到dp[i][]),因此不妨直接開一個一維陣列dp[v](即把第一維省去),列舉方向改變為i從1到n,v從V到0(逆序!),狀態轉移方程改變為 d p [ v ] = m a x { d p [ v ] , d p [ v − w [ i ] + c [ i ] } ( 1 ≤ i ≤ n , w [ i ] ≤ v ≤ V ) dp[v]=max\left \{ dp[v] , dp[v-w[i]+c[i] \right \}\left (1\le i\le n,w[i]\le v \le V \right ) dp[v]=max{dp[v],dp[vw[i]+c[i]}(1in,w[i]vV)。注意v的列舉順序是從右往左,這樣的技巧稱為滾動陣列,優化的空間複雜度為 O ( V ) O(V) O(V)

for(int i = 1; i <= n; i++){    
    for(int v = V; v >= w[i]; v--){ 
        dp[v]=max(dp[v],dp[v-w[i]+c[i]);
    }
}

特別說明:如果是二維陣列存放,v的列舉是順序還是逆序都無所謂;如果使用一維陣列存放,則v的列舉必須是逆序。

完全揹包問題

有n件物品,每件物品的單件重量為w[i],價值為c[i]。現有一個容量為V的揹包,問如何選取物品放入揹包,使得揹包內物品的總價值最大。其中每種物品都有無窮件。

和01揹包一樣,令dp[i][v]表示前i件物品恰好裝入容量為v的揹包中所能獲得的最大價值。考慮對第i件物品的選擇策略,有兩種策略:

  1. 不放第i件物品,那麼dp[i][v] = dp[i-1][v],這一步和01揹包一樣
  2. 放第i件物品,並不是轉移到dp[i-1][v-w[i]]這個狀態,而是轉移到dp[i][v-w[i]],因為放了第i件物品後還可以繼續放第i件物品,直到第二維的v-w[i]無法保持大於等於0為止。

狀態轉移方程為 d p [ i ] [ v ] = m a x { d p [ i − 1 ] [ v ] , d p [ i ] [ v − w [ i ] ] + c [ i ] } ( 1 ≤ i ≤ n , w [ i ] ≤ v ≤ V ) dp[i][v]=max\left \{ dp[i-1][v] , dp[i][v-w[i]]+c[i] \right \}\left (1\le i\le n,w[i]\le v \le V \right ) dp[i][v]=max{dp[i1][v],dp[i][vw[i]]+c[i]}(1in,w[i]vV)

邊界為 d p [ 0 ] [ v ] = 0 ( 0 ≤ v ≤ V ) dp[0][v]=0(0\le v \le V) dp[0][v]=0(0vV)

一維形式的狀態轉移方程為 d p [ v ] = m a x { d p [ v ] , d p [ v − w [ i ] + c [ i ] } ( 1 ≤ i ≤ n , w [ i ] ≤ v ≤ V ) dp[v]=max\left \{ dp[v] , dp[v-w[i]+c[i] \right \}\left (1\le i\le n,w[i]\le v \le V \right ) dp[v]=max{dp[v],dp[vw[i]+c[i]}(1in,w[i]vV)

邊界為 d p [ 0 ] [ v ] = 0 ( 0 ≤ v ≤ V ) dp[0][v]=0(0\le v \le V) dp[0][v]=0(0vV)

寫成一維形式後和01揹包完全相同,唯一區別在於這裡v的列舉順序是正向列舉,而01揹包的一維形式中v必須是逆向列舉。

for(int i = 1; i <= n; i++){    
    for(int v = w[i]; v <= V; v++){ 
        dp[v]=max(dp[v],dp[v-w[i]+c[i]);
    }
}

總結

(1)最大連續子序列和

令狀態dp[i]表示以A[i]作為末尾的連續序列的最大和

(2)最長不下降子序列(LIS)

dp[i]表示以A[i]結尾的最長不下降子序列長度

(3)最長公共子序列(LCS)

dp[i][j]表示字串A的i號位和字串B的j號位之前的LCS長度

(4)最長迴文子串

dp[i][j]表示S[i]S[j]所表示的子串是否是迴文子串

(5)數塔DP

dp[i][j]表示從第i行第j個數字出發的到達最底層的所有路徑中能得到的最大和

(6)DAG最長路
dp[i]表示從i號頂點出發能獲得的最長路徑長度

(7)01揹包

dp[i][v]表示前i件物品恰好裝入容量為v的揹包中所能獲得的最大價值

(8)完全揹包

dp[i][v]表示前i件物品恰好裝入容量為v的揹包中所能獲得的最大價值

  • 1~4:當題目與序列或者字串(記為A)有關時,可以考慮把狀態設計成下面兩種形式,然後根據端點特點去考慮狀態轉移方程。其中XXX均為原問題的表述

    1. dp[i]表示以A[i]結尾(或開頭)的XXX。
    2. dp[i][j]表示A[i]A[j]區間的XXX。
  • 5~8:它們的狀態設計都包含了某種“方向”的意思,那麼分析題目中的狀態需要幾維來表示,然後對其中的每一維採取下面的某一個表述

    1. 恰好為i
    2. 前i

    在每一維的含義設定完畢後,dp陣列的含義可以設定成“令dp陣列表示恰好為i(或前i)、恰好為j(或前j)…的XXX”,接下來通過端點的特點去考慮狀態轉移方程。

相關文章