leetcode 516. Longest Palindromic Subsequence 動態規劃優化問題
0 這個問題是求一個串中,迴文子串的最長的長度。
1 分析
首先分析是否能分解為子問題,s[0,i]與s[0,i+1]是否有關聯?有關聯,因為對s[0,i]後面加上一個字元x後,字元x可能是一個迴文串的最後一個字元,從而造成迴文串的增長。在串s[0,i]後面加上一個字元x後,挨個與之前的串進行比較,直到有字元s[a] == x;那麼從s[a,i+i]這部分的迴文串就可以進行延長。按照這種思路,我們需要用一個二維陣列來進行記錄兩兩下標之間的最大回文串長度。
2 遞迴式以及時間複雜度
用dp[i][j]來記錄,從字串s[i,j]的迴文串的最長的值。 dp[i][j] = dp[i+1][j-1] + 2; if s[i] == s[j] = max(dp[i+1][j], dp[i][j-1]) else 時間複雜度:o(n^2),空間複雜度:o(n^2) 時間複雜度解釋,對二維陣列進行窮舉,需要覆蓋全部的取值 空間複雜度解釋,需要填充dp矩陣的三角形//程式碼在最後
3 優化
在時間上,問題沒有優化空間,在空間上,有優化部分。為什麼?因為我們看到遞迴式中dp[i][j]的求法,他只和區域性的資料項有關,確切來說只有附近的三個值有關。進一步說,dp[i+1][j+1] = f(s[i],s[j], dp[i][j], dp[i+1][j], dp[i][j+1]) dp[i+1][j+1]是他附近上一項的函式。所以我們再這裡可以進行優化為兩個長度為n的陣列進行表示。
4 程式碼
//空間複雜度為 o(n^2) int longestPalindromeSubseq2(string s) { int n = s.size(); if (n <= 1) return n; vector<vector<int>> dp(n, vector<int>(n, 0)); for (int i = 0; i < n - 1; ++i) { dp[i][i] = 1; dp[i][i + 1] = 1 + (s[i] == s[i + 1]); } dp[n - 1][n - 1] = 1; for (int i = 2; i < n; ++i) for (int j = 0; j < n - i; ++j) { int row = j; int col = i + j; if (s[row] == s[col]) dp[row][col] = dp[row + 1][col - 1] + 2; else dp[row][col] = max(dp[row + 1][col], dp[row][col - 1]); } return dp[0][n - 1]; } //空間複雜度為o(n) int longestPalindromeSubseq(string s) { int n = s.size(); if (n <= 1) return n; vector<vector<int>> dp(n, vector<int>(2, 0)); for (int i = 0; i < n - 1; ++i) { dp[i][0] = 1; dp[i][1] = 1 + (s[i] == s[i + 1]); } dp[n - 1][0] = 1; for (int i = 2; i < n; ++i) for (int j = 0; j < n - 1; ++j) { bool flag = i % 2; int row = j; int col = i + j; if (s[row] == s[col]) dp[j][flag] = dp[j + 1][flag] + 2; else dp[j][flag] = max(dp[j][!flag], dp[j + 1][!flag]); } return max(dp[0][0], dp[0][1]); }
5 程式碼分析
5.1 二維陣列類似這個圖,遍歷時為對角遍歷,這樣可以不用遞迴求dp[i][j](直接用帶memo的方法,可以寫一個遞迴函式,一步求dp[0][n-1]也可以),遍歷順序為,黃色為i=0,灰色為i=1,橙色為i=2。我們再圖中可以看到,在求dp[i][j]的時候,只用到了附近幾個節點的資料,所以這個題目可以僅用兩個陣列來進行求解。另外,在對角遍歷的時候,可以把遍歷的方法記住,外層i迴圈的範圍,內層j迴圈的範圍,已經對應的row和col,在記住之後可以幫助提升程式設計效率。
5.2 在用陣列的時候,可以建立兩個dp來進行相互賦值,也可以利用上面的滾動陣列進行方便書寫量的減少。不過這都是實現細節,用自己熟悉的方法即可,主要是演算法思想的掌握。