1. 程式人生 > 實用技巧 >516. Longest Palindromic Subsequence

516. Longest Palindromic Subsequence

問題:

給定一個字串,求其中最長迴文子序列(子序列不是連續字串)的長度。

Example 1:
Input:
"bbbab"
Output:
4
One possible longest palindromic subsequence is "bbbb".
 
Example 2:
Input:
"cbbd"
Output:
2
One possible longest palindromic subsequence is "bb".
 
Constraints:
1 <= s.length <= 1000
s consists only of lowercase English letters.

  

解法:DP(動態規劃)

1.確定【狀態】:字串s的

  • 第i個字元:s[i]
  • 第j個字元:s[j]

2.確定【選擇】:分兩種情況

  • s[i] == s[j]:
    • 前一個子串狀態<不包含當前這兩個字元s[i]s[j]>(公共序列長度)+2: dp[i+1][j-1] + 2
  • s[i] != s[j]:有以下2種情況,取最大值。
    • 只有s[i]是最終最長迴文子序列的一個字元 -> =上一個包含s[i]而不包含s[j]的字元狀態:dp[i][j-1]
    • 只有s[j]是最終最長迴文子序列的一個字元->=上一個不包含s[i]而包含s[j]的字元狀態:dp[i+1][j]
    • 兩個字元都不是最終最長迴文子序列的一個字元 ->=上一個既不包含s[i]又不包含s[j]的字元狀態:dp[i+1][j-1]
      • ★由於dp[i+1][j-1]一定<=dp[i][j-1] or dp[i+1][j],因此可以省略比較dp[i+1][j-1]

3. dp[i][j]的含義:

字串s的第 i 個字元到第 j 個字元為止,這段子串中,存在最長迴文子序列的長度。

4. 狀態轉移:

dp[i][j]=

  • (s[i] == s[j]):=前一個子串狀態+2:dp[i+1][j-1] + 2
  • (s[i] != s[j]):=max {
    • 上一個包含s[i]字元的狀態:dp[i][j-1]
    • 上一個包含s[j]字元的狀態:dp[i+1][j]
    • 上一個s[i]s[j]都不包含的狀態:dp[i+1][j-1](★可省略) }

5. base case:

  • dp[i][i]=1:單文字子串,最長迴文子序列為它自己,長度為 1。

6. 遍歷順序:

根據狀態轉移公式,在求得dp[i][j]之前,需先求得dp[i+1][j-1],dp[i+1][j],dp[i][j-1]

因此需要:i:大->小,j:小->大 遍歷

⚠️ 注意:本問題,i一定<=j,因此dp[i][j]中i>j的memory基本不會被用到。

只有在base case計算dp[i][i+1]且s[i]==s[i+1]的時候,作為★dp[i+1][j-1]被用到。應該為0。這也是在後面♻️ 優化處pre的賦值理由。

程式碼參考:

 1 class Solution {
 2 public:
 3     //dp[i][j]: in substring: s[i~j], the length of LPS.
 4     //case_1: s[i]==s[j]:dp[i+1][j-1] + 2
 5     //        add 2 to the pre status(which both not include s[i]s[j]) dp[i+1][j-1]
 6     //case_2: s[i]!=s[j]: max of following 2 case:
 7     //        case_2_1: the pre status(only include s[i],s[i] is in LPS). dp[i][j-1]
 8     //        case_2_2: the pre status(only include s[j],s[j] is in LPS). dp[i+1][j]
 9     //base case:
10     //dp[i][i]:1
11     int longestPalindromeSubseq(string s) {
12         int n = s.length();
13         vector<vector<int>> dp(n, vector<int>(n, 0));
14         for(int i=0; i<n; i++) {
15             dp[i][i] = 1;
16         }
17         for(int i=n-1; i>=0; i--) {
18             for(int j=i+1; j<n; j++) {
19                 if(s[i]==s[j]) {
20                     dp[i][j] = dp[i+1][j-1] +2;
21                 } else {
22                     dp[i][j] = max(dp[i][j-1], dp[i+1][j]);
23                 }
24             }
25         }
26         return dp[0][n-1];
27     }
28 };

♻️ 優化:

空間複雜度:2維->1維

去掉 i

壓縮所有行到一行。

左下角dp[i+1][j-1]會被上面的dp[i][j-1]覆蓋,因此引入變數pre,在更新dp[i][j-1]之前,儲存dp[i+1][j-1]

程式碼參考:

 1 class Solution {
 2 public:
 3     int longestPalindromeSubseq(string s) {
 4         int n = s.length();
 5         vector<int> dp(n, 0);
 6         for(int i=n-1; i>=0; i--) {
 7             int pre = 0;
 8             //base case:for s[i~i+1]->e.g.'aa',s[i]=s[j]->dp[j]=pre+2 (should)=2->pre=0
 9             dp[i] = 1;//base case
10             for(int j=i+1; j<n; j++) {
11                 int tmp = dp[j];
12                 if(s[i]==s[j]) {
13                     dp[j] = pre +2;
14                 } else {
15                     dp[j] = max(dp[j-1], dp[j]);
16                 }
17                 pre = tmp;
18             }
19         }
20         return dp[n-1];
21     }
22 };