状态压缩技巧:动态规划的降维打击
本文由labuladong原創,本博文僅作為知識點學習,不會用于任何商業用途!
動態規劃技巧對于算法效率的提升非常可觀,一般來說都能把指數級和階乘級時間復雜度的算法優化成 O(N^2),堪稱算法界的二向箔,把各路魑魅魍魎統統打成二次元。
但是,動態規劃本身也是可以進行階段性優化的,比如說我們常聽說的「狀態壓縮」技巧,就能夠把很多動態規劃解法的空間復雜度進一步降低,由 O(N^2) 降低到 O(N),
能夠使用狀態壓縮技巧的動態規劃都是二維dp問題,你看它的狀態轉移方程,如果計算狀態dp[i][j]需要的都是dp[i][j]相鄰的狀態,那么就可以使用狀態壓縮技巧,將二維的dp數組轉化成一維,將空間復雜度從 O(N^2) 降低到 O(N)。
什么叫「和dp[i][j]相鄰的狀態」呢,比如前文 最長回文子序列 中,最終的代碼如下:
int longestPalindromeSubseq(string s) {
int n = s.size();
// dp 數組全部初始化為 0
vector<vector<int>> dp(n, vector<int>(n, 0));
// base case
for (int i = 0; i < n; i++)
dp[i][i] = 1;
// 反著遍歷保證正確的狀態轉移
for (int i = n - 2; i >= 0; i--) {
for (int j = i + 1; j < n; j++) {
// 狀態轉移方程
if (s[i] == s[j])
dp[i][j] = dp[i + 1][j - 1] + 2;
else
dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]);
}
}
// 整個 s 的最長回文子串長度
return dp[0][n - 1];
}
PS:我們本文不探討如何推狀態轉移方程,只探討對二維 DP 問題進行狀態壓縮的技巧。技巧都是通用的,所以如果你沒看過前文,不明白這段代碼的邏輯也無妨,完全不會阻礙你學會狀態壓縮。
你看我們對dp[i][j]的更新,其實只依賴于dp[i+1][j-1], dp[i][j-1], dp[i+1][j]這三個狀態:
這就叫和dp[i][j]相鄰,反正你計算dp[i][j]只需要這三個相鄰狀態,其實根本不需要那么大一個二維的 dp table 對不對?
狀態壓縮的核心思路就是,將二維數組「投影」到一維數組:
思路很直觀,但是也有一個明顯的問題,圖中dp[i][j-1]和dp[i+1][j-1]這兩個狀態處在同一列,而一維數組中只能容下一個,那么當我計算dp[i][j]時,他倆必然有一個會被另一個覆蓋掉,怎么辦?
這就是狀態壓縮的難點,下面就來分析解決這個問題,還是拿「最長回文子序列」問題舉例,它的狀態轉移方程主要邏輯就是如下這段代碼:
for (int i = n - 2; i >= 0; i--) {
for (int j = i + 1; j < n; j++) {
// 狀態轉移方程
if (s[i] == s[j])
dp[i][j] = dp[i + 1][j - 1] + 2;
else
dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]);
}
}
想把二維dp數組壓縮成一維,一般來說是把第一個維度,也就是i這個維度去掉,只剩下j這個維度。壓縮后的一維dp數組就是之前二維dp數組的dp[i][..]那一行。
我們先將上述代碼進行改造,直接無腦去掉i這個維度,把dp數組變成一維:
for (int i = n - 2; i >= 0; i--) {
for (int j = i + 1; j < n; j++) {
// 在這里,一維 dp 數組中的數是什么?
if (s[i] == s[j])
dp[j] = dp[j - 1] + 2;
else
dp[j] = max(dp[j], dp[j - 1]);
}
}
上述代碼的一維dp數組只能表示二維dp數組的一行dp[i][..],那我怎么才能得到dp[i+1][j-1], dp[i][j-1], dp[i+1][j]這幾個必要的的值,進行狀態轉移呢?
在代碼中注釋的位置,將要進行狀態轉移,更新dp[j],那么我們要來思考兩個問題:
1、在對dp[j]賦新值之前,dp[j]對應著二維dp數組中的什么位置?
2、dp[j-1]對應著二維dp數組中的什么位置?
對于問題 1,在對dp[j]賦新值之前,dp[j]的值就是外層 for 循環上一次迭代算出來的值,也就是對應二維dp數組中dp[i+1][j]的位置。
對于問題 2,dp[j-1]的值就是內層 for 循環上一次迭代算出來的值,也就是對應二維dp數組中dp[i][j-1]的位置。
那么問題已經解決了一大半了,只剩下二維dp數組中的dp[i+1][j-1]這個狀態我們不能直接從一維dp數組中得到:
for (int i = n - 2; i >= 0; i--) {
for (int j = i + 1; j < n; j++) {
if (s[i] == s[j])
// dp[i][j] = dp[i+1][j-1] + 2;
dp[j] = ?? + 2;
else
// dp[i][j] = max(dp[i+1][j], dp[i][j-1]);
dp[j] = max(dp[j], dp[j - 1]);
}
}
因為 for 循環遍歷i和j的順序為從左向右,從下向上,所以可以發現,在更新一維dp數組的時候,dp[i+1][j-1]會被dp[i][j-1]覆蓋掉,圖中標出了這四個位置被遍歷到的次序:
那么如果我們想得到dp[i+1][j-1],就必須在它被覆蓋之前用一個臨時變量temp把它存起來,并把這個變量的值保留到計算dp[i][j]的時候。為了達到這個目的,結合上圖,我們可以這樣寫代碼:
for (int i = n - 2; i >= 0; i--) {
// 存儲 dp[i+1][j-1] 的變量
int pre = 0;
for (int j = i + 1; j < n; j++) {
int temp = dp[j];
if (s[i] == s[j])
// dp[i][j] = dp[i+1][j-1] + 2;
dp[j] = pre + 2;
else
dp[j] = max(dp[j], dp[j - 1]); // 到下一輪循環,pre 就是 dp[i+1][j-1] 了
pre = temp;
}
}
別小看這段代碼,這是一維dp最精妙的地方,會者不難,難者不會。為了清晰起見,我用具體的數值來拆解這個邏輯:
假設現在i = 5, j = 7且s[5] == s[7],那么現在會進入下面這個邏輯對吧:
if (s[5] == s[7])
// dp[5][7] = dp[i+1][j-1] + 2;
dp[7] = pre + 2;
我問你這個pre變量是什么?是內層 for 循環上一次迭代的temp值。
那我再問你內層 for 循環上一次迭代的temp值是什么?是dp[j-1]也就是dp[6],但這是外層 for 循環上一次迭代對應的dp[6],也就是二維dp數組中的dp[i+1][6] = dp[6][6]。
也就是說,pre變量就是dp[i+1][j-1] = dp[6][6],也就是我們想要的結果。
那么現在我們成功對狀態轉移方程進行了降維打擊,算是最硬的的骨頭啃掉了,但注意到我們還有 base case 要處理呀:
// 二維 dp 數組全部初始化為 0
vector<vector<int>> dp(n, vector<int>(n, 0));
// base case
for (int i = 0; i < n; i++)
dp[i][i] = 1;
如何把 base case 也打成一維呢?很簡單,記住,狀態壓縮就是投影,我們把 base case 投影到一維看看:
二維dp數組中的 base case 全都落入了一維dp數組,不存在沖突和覆蓋,所以說我們直接這樣寫代碼就行了:
// 一維 dp 數組全部初始化為 1
vector<int> dp(n, 1);
至此,我們把 base case 和狀態轉移方程都進行了降維,實際上已經寫出完整代碼了:
int longestPalindromeSubseq(string s) {
int n = s.size();
// base case:一維 dp 數組全部初始化為 1
vector<int> dp(n, 1);
for (int i = n - 2; i >= 0; i--) {
int pre = 0;
for (int j = i + 1; j < n; j++) {
int temp = dp[j];
// 狀態轉移方程
if (s[i] == s[j])
dp[j] = pre + 2;
else
dp[j] = max(dp[j], dp[j - 1]);
pre = temp;
}
}
return dp[n - 1];
}
本文就結束了,不過狀態壓縮技巧再牛逼,也是基于常規動態規劃思路之上的。
你也看到了,使用狀態壓縮技巧對二維dp數組進行降維打擊之后,解法代碼的可讀性變得非常差了,如果直接看這種解法,任何人都是一臉懵逼的。
算法的優化就是這么一個過程,先寫出可讀性很好的暴力遞歸算法,然后嘗試運用動態規劃技巧優化重疊子問題,最后嘗試用狀態壓縮技巧優化空間復雜度。
也就是說,你最起碼能夠熟練運用我們前文 動態規劃框架套路詳解 的套路找出狀態轉移方程,寫出一個正確的動態規劃解法,然后才有可能觀察狀態轉移的情況,分析是否可能使用狀態壓縮技巧來優化。
希望讀者能夠穩扎穩打,層層遞進,對于這種比較極限的優化,不做也罷。畢竟套路存于心,走遍天下都不怕!
本文由labuladong原創,本博文僅作為知識點學習,不會用于任何商業用途!
Tips:時間會解決一切
總結
以上是生活随笔為你收集整理的状态压缩技巧:动态规划的降维打击的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Angular Jasmine单元测试用
- 下一篇: 笔记本分游戏本和什么本(高端轻薄本和高端