【PAT】5. 動態規劃
【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(1≤i≤j≤n),使得 A i + . . . + A j A_i+...+A_j Ai+...+Aj最大,輸出這個最大和。
- 暴力解法( 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)複雜度
- 記錄字首和( 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[i−1]
- 動態規劃(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]來說就會有兩種可能:
- 如果存在A[i]之前的元素A[j](j < i),使得
A[j]<=A[i]
且dp[j] + 1 > dp[i]
(即把A[i]跟在以A[j]結尾的LIS後面,形成一條更長的不下降子序列) - 如果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]的情況,分為兩種決策:
- 若
A[i]==B[j]
,則字串S與字串B的LCS增加了一位,有dp[i][j] = d[i-1][j-1] + 1
。 - 若
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]}
。 - 邊界:
dp[i][0] = dp[0][j] = 0
最長迴文子串 3
給出一個字串S,求S的最長迴文子串的長度
令dp[i][j]表示S[i]至S[j]所表示的子串是否是迴文子串,是則為1,不是為0。根據S[i]是否等於S[j],可以把轉移情況分為兩類:
- 若
S[i]==S[j]
,那麼只要S[i+1]至S[j-1]是迴文子串,S[i]至S[j]就是迴文子串,令dp[i][j] = dp[i+1][j-1]
- 若S[i]!=S[j],那麼一定不是迴文子串,令
d[i][j]=0
- 邊界:
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[i→j]∣(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[j→i]∣(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件物品的選擇策略,有兩種策略:
- 不放第i件物品,那麼問題轉化為前i-1件物品恰好裝入容量為v的揹包中所能獲得的最大價值,也即
dp[i-1][v]
- 放第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[i−1][v],dp[i−1][v−w[i]]+c[i]}(1≤i≤n,w[i]≤v≤V);
邊界為 d p [ 0 ] [ v ] = 0 ( 0 ≤ v ≤ V ) dp[0][v]=0\left (0\le v\le V \right ) dp[0][v]=0(0≤v≤V)(即前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](0≤v≤V),取其最大值才是最後的結果。時間複雜度為
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[v−w[i]+c[i]}(1≤i≤n,w[i]≤v≤V)。注意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件物品的選擇策略,有兩種策略:
- 不放第i件物品,那麼
dp[i][v] = dp[i-1][v]
,這一步和01揹包一樣 - 放第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[i−1][v],dp[i][v−w[i]]+c[i]}(1≤i≤n,w[i]≤v≤V)
邊界為 d p [ 0 ] [ v ] = 0 ( 0 ≤ v ≤ V ) dp[0][v]=0(0\le v \le V) dp[0][v]=0(0≤v≤V)
一維形式的狀態轉移方程為 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[v−w[i]+c[i]}(1≤i≤n,w[i]≤v≤V)
邊界為 d p [ 0 ] [ v ] = 0 ( 0 ≤ v ≤ V ) dp[0][v]=0(0\le v \le V) dp[0][v]=0(0≤v≤V)
寫成一維形式後和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均為原問題的表述
- 令
dp[i]
表示以A[i]
結尾(或開頭)的XXX。 - 令
dp[i][j]
表示A[i]
至A[j]
區間的XXX。
- 令
-
5~8:它們的狀態設計都包含了某種“方向”的意思,那麼分析題目中的狀態需要幾維來表示,然後對其中的每一維採取下面的某一個表述
- 恰好為i
- 前i
在每一維的含義設定完畢後,dp陣列的含義可以設定成“令dp陣列表示恰好為i(或前i)、恰好為j(或前j)…的XXX”,接下來通過端點的特點去考慮狀態轉移方程。
相關文章
- 動態規劃動態規劃
- [leetcode] 動態規劃(Ⅰ)LeetCode動態規劃
- 動態規劃法動態規劃
- 模板 - 動態規劃動態規劃
- 動態規劃初步動態規劃
- 動態規劃分析動態規劃
- 動態規劃(DP)動態規劃
- PAT1040 Longest Symmetric String (25分) 中心擴充套件法+動態規劃套件動態規劃
- 演算法系列-動態規劃(1):初識動態規劃演算法動態規劃
- 動態規劃小結動態規劃
- [leetcode 1235] [動態規劃]LeetCode動態規劃
- 動態規劃專題動態規劃
- 動態規劃-----線性動態規劃
- 好題——動態規劃動態規劃
- 動態規劃初級動態規劃
- 淺談動態規劃動態規劃
- 3.動態規劃動態規劃
- 動態規劃題單動態規劃
- 動態規劃 總結動態規劃
- 雙序列動態規劃動態規劃
- 動態規劃方法論動態規劃
- [atcoder 358] 【動態規劃】動態規劃
- 區間動態規劃動態規劃
- 動態規劃(Dynamic programming)動態規劃
- 有關動態規劃動態規劃
- 動態規劃之數的劃分動態規劃
- 禮物的最大價值(一維動態規劃&二維動態規劃)動態規劃
- leetcode題解(動態規劃)LeetCode動態規劃
- [動態規劃] 區間 dp動態規劃
- (C++)DP動態規劃C++動態規劃
- 【CodeChef】Graph Cost(動態規劃)動態規劃
- leetcode總結——動態規劃LeetCode動態規劃
- 動態規劃練習題動態規劃
- 大盜阿福(動態規劃)動態規劃
- 動態規劃做題思路動態規劃
- 動態規劃入門篇動態規劃
- 演算法-動態規劃演算法動態規劃
- 動態規劃(未完成)動態規劃