LeetCode 730. Count Different Palindromic Subsequences (區間DP)

題意

給一個字符串S,求它所有子序列不同非空迴文串的數量。字符串由 'a' 'b' 'c' 'd' 四個字母組成。

 

由於題目要求的是不同迴文串。 abba 的迴文串子序列爲 a,b,aba,abba 其中 aba 只能算一次。

 

最近做 區間DP 的題,習慣起手寫

for (int l = 0; l < n; l++) {
    for (int s = 0; s + l < n; s++) {
        int e = s + l;
        .....
    }
}    

外層枚舉區間長度,內層枚舉區間起始位置。

 

設 dp[i][j] 表示  S[i...j] 子序列中包含的迴文串的數量。

 

解法1、枚舉迴文串第一個字母

設迴文串第一個字母是 a 則最後一個字母也要是 a 然後找到在區間 [s,t] 中第一個和最後一個 a 的位置 x 和 y ,則,首字母爲 a 的迴文串個數爲 dp[x-1][y+1] 

因爲,如果使用的不是兩邊的 a 而是內部的 a 作爲邊界,能發現內部 a 做邊界組成的迴文串 兩邊的 a 做邊界時都組成。

最後再用兩邊的 a 組成 aa 以及 單個 a。當範圍內只存在一個 a 時,則能貢獻的子串只有 1 個。

 

很難受的是直接遞推超時了,遞歸才勉強能過。。。

class Solution {
public:
    int countPalindromicSubsequences(string S) {
        // dp[i][j] 表示 s[i...j] 包含的子序列個數
        int n = S.size();
        vector<vector<int>> pos(4, vector<int>());
        vector<vector<int>> dp(n, vector<int>(n, 0));
        for (int i = 0; i < n; i++) {
            pos[S[i] - 'a'].push_back(i);
        }
        return dfs(S, pos, dp, 0, n - 1);
    }
    int dfs(string &S, vector<vector<int>> &pos, vector<vector<int>> &dp, int s, int t) {
        if (s + 1 >= t) return t - s + 1;
        if (dp[s][t]) return dp[s][t];
        int ans = 0;
        for (int i = 0; i < 4; i++) {
            if (pos[i].empty()) continue;
            auto first_pos = lower_bound(pos[i].begin(), pos[i].end(), s);
            auto end_pos = upper_bound(pos[i].begin(), pos[i].end(), t) - 1;
            if (first_pos == pos[i].end() || *first_pos > t) continue;
            int f = *first_pos, e = *end_pos;
            ans = (ans + 1) % 1000000007;
            if (f != e) ans = (ans + 1) % 1000000007;
            if (f + 1 < e) {
                ans = (ans + dfs(S, pos, dp, f + 1, e - 1)) % 1000000007;
            }
        }
        return dp[s][t] = ans;
    }
};

 

解法2 (https://leetcode.com/problems/count-different-palindromic-subsequences/discuss/109507/Java-96ms-DP-Solution-with-Detailed-Explanation

求 S[s..e] 的迴文串數

當 S[s] != S[e] 時,可得公式

dp[s][e] = dp[s+1][e] + dp[s][e-1] - dp[s+1][e-1];

即 (包含s + 兩邊都不含的) 和 (包含e+ 兩邊都不含的)- 兩邊都不含的

 

當 S[s] == S[e] 時 還用上面的公式會出現重複。

比如 aaa ,計算 dp[0][2] 時 ,dp[0][1] 包含 (aa) 和 dp[1][2] 包含的 aa 重複但是沒有被減去。

而如果想要減去重複部分,就要找到和邊界位置相同的字母。假設邊界字母是 a,

 

當沒有和邊界位置相同的字母, dp[s][e] = dp[s+1][e-1](兩邊都不含的)+ dp[s+1][e-1](兩邊都不含的 + 兩邊的字母)+ 1 (兩邊的字母組成 aa) + 1 (單個首字母 a)

當存在和邊界位置相同的字母,且只有一個,dp[s][e] = dp[s+1][e-1](兩邊都不含的)+ dp[s+1][e-1](兩邊都不含的 + 兩邊的字母)+ 1 (兩邊的字母組成 aa)

當存在和邊界位置相同的字母,且有多個(2個及以上) 中間的組成的,可能會和中間的a加上邊界之後重複,比如 a(aaa)a 中間組成a aa aaa 加上兩邊的a 就變成了 aaa aaaa aaaaa 那麼 aaa就重複計算了,找到除了邊界的兩個a,最外層的兩個a,設位置是 low 和 high 那麼,dp[low+1][high-1] 中所有子序列和low high 組成的迴文串與和s e 組成的迴文串都會重複。所以要減去dp[low+1][high-1]。dp[s][e] = dp[s+1][e-1] * 2 - dp[low + 1][high - 1]; 

 

其實我也不是很懂。。。。瞎寫的。。。真的好難啊。。。。 

class Solution {
public:
    int countPalindromicSubsequences(string S) {
        int n = S.size();
        int dp[n][n];
        int mod = 1e9 + 7;
        for (int l = 0; l < n; l++) {
            for (int s = 0; s + l < n; s++) {
                int e = s + l;
                if (l <= 1) {
                    dp[s][e] = l + 1;
                } else if (S[s] == S[e]) {
                    int low = s + 1, high = e - 1;
                    // 尋找最近的和 S[s](S[e]) 相同的字母的位置
                    while (low <= high && S[low] != S[e]) low++;
                    while (high >= low && S[high] != S[e]) high--;
                    
                    if (low > high) dp[s][e] = dp[s+1][e-1] * 2 + 2;
                    else if (low == high) dp[s][e] = dp[s+1][e-1] * 2 + 1;
                    else dp[s][e] = dp[s+1][e-1] * 2 - (low + 1 < high ? dp[low + 1][high - 1] : 0);
                } else {
                    dp[s][e] = dp[s+1][e] + dp[s][e-1] - dp[s+1][e-1];
                }
                dp[s][e] = (dp[s][e] % mod + mod) % mod;
            }
        }
        return dp[0][n - 1];
    }
};

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章