Want Help Cracking FAANG?

(Then click this)

×
Back to Question Bank

730. Count Different Palindromic Subsequences - Leetcode Solution

Problem Description

Given a string s, count how many different non-empty palindromic subsequences exist in s. Since the answer may be very large, return it modulo 10^9 + 7.

  • A subsequence is a sequence that can be derived from the string by deleting some (or no) characters without changing the order of the remaining characters.
  • A palindrome is a sequence that reads the same backward as forward.
  • You must count distinct palindromic subsequences; duplicate subsequences (even if formed from different positions) are only counted once.
  • Constraints: 1 <= s.length <= 1000; s consists only of lowercase English letters.

Thought Process

At first, it might seem natural to try generating all possible subsequences and checking which ones are palindromes. However, this brute-force method is impractical because the number of subsequences grows exponentially with the length of s. For example, a string of length 20 has over a million subsequences.

Instead, we need an efficient way to count all unique palindromic subsequences. Dynamic programming is a good fit for this problem because:

  • We can break the problem into smaller subproblems—counting palindromic subsequences in substrings.
  • We can store results for substrings to avoid redundant computation.
The main challenge is to count only distinct palindromic subsequences and not double-count those formed from different positions but with the same characters.

Solution Approach

We use dynamic programming with a 2D table dp[i][j], where each entry represents the number of different palindromic subsequences in the substring s[i..j].

  1. Base Case: Every single character is a palindrome, so for all i, dp[i][i] = 1.
  2. Recurrence: For substrings longer than 1:
    • If s[i] != s[j], then dp[i][j] = dp[i+1][j] + dp[i][j-1] - dp[i+1][j-1]. This counts palindromic subsequences ending at j, starting at i, and subtracts the overlap.
    • If s[i] == s[j], things get more interesting:
      • Let low be the first occurrence of s[i] after i, and high be the last occurrence of s[j] before j.
      • If there are no other s[i] between i and j, then: dp[i][j] = 2 * dp[i+1][j-1] + 2 (one for the single character, one for the pair).
      • If there is one occurrence, then: dp[i][j] = 2 * dp[i+1][j-1] + 1.
      • If there are more, then: dp[i][j] = 2 * dp[i+1][j-1] - dp[low+1][high-1] (subtract the overlapping palindromic subsequences counted twice).
  3. Modulo Operation: Since the answer can be large, we take modulo 10^9 + 7 at every step.
  4. Final Answer: dp[0][n-1] gives the number of distinct palindromic subsequences for the entire string.

We also use pre-processing to quickly find the next and previous occurrences of each character, which helps in efficiently determining low and high.

Example Walkthrough

Let's use the input s = "bccb":

  • Single characters: "b", "c" (each at least once).
  • Two-character palindromes: "bb", "cc".
  • Three-character palindromes: "bcb".
  • Four-character palindrome: none, since "bccb" is not a palindrome.

We fill the dp table diagonally, starting with substrings of length 1, then 2, and so on. At each step, we use the recurrence to count palindromic subsequences, ensuring we only count unique ones.

For i=0, j=3 ("bccb"), since s[0]==s[3] ("b"), we look for the next and previous occurrence of "b" between 0 and 3. The only other "b" is at 0 and 3, so there are no others between them. Thus, dp[0][3] = 2 * dp[1][2] + 2. dp[1][2] is for "cc", which is 2 (for "c" and "cc"). So dp[0][3] = 2*2 + 2 = 6.

The distinct palindromic subsequences are: "b", "c", "bb", "cc", "bcb", "bccb".

Time and Space Complexity

  • Brute-force: Generating all subsequences takes O(2^n) time and space, which is infeasible for n > 20.
  • Optimized DP Approach:
    • We fill an n x n table, where n is the length of s, so time complexity is O(n^2).
    • For each cell, finding low and high can be done in O(1) with preprocessing, or O(n) otherwise.
    • Space complexity is also O(n^2) for the DP table.

Summary

This problem is a classic application of dynamic programming for counting unique palindromic subsequences. The key insight is to use a DP table to avoid redundant computation and to carefully handle overlapping cases using inclusion-exclusion principles. Efficient pre-processing for character positions helps achieve an optimal O(n^2) solution, making it feasible even for strings of length up to 1000.

Code Implementation

class Solution:
    def countPalindromicSubsequences(self, s: str) -> int:
        MOD = 10**9 + 7
        n = len(s)
        dp = [[0] * n for _ in range(n)]

        for i in range(n):
            dp[i][i] = 1

        for length in range(2, n + 1):
            for i in range(n - length + 1):
                j = i + length - 1
                if s[i] != s[j]:
                    dp[i][j] = (dp[i+1][j] + dp[i][j-1] - dp[i+1][j-1]) % MOD
                else:
                    low = i + 1
                    high = j - 1
                    while low <= high and s[low] != s[i]:
                        low += 1
                    while low <= high and s[high] != s[j]:
                        high -= 1
                    if low > high:
                        dp[i][j] = (2 * dp[i+1][j-1] + 2) % MOD
                    elif low == high:
                        dp[i][j] = (2 * dp[i+1][j-1] + 1) % MOD
                    else:
                        dp[i][j] = (2 * dp[i+1][j-1] - dp[low+1][high-1]) % MOD
                if dp[i][j] < 0:
                    dp[i][j] += MOD
        return dp[0][n-1]
      
class Solution {
public:
    int countPalindromicSubsequences(string s) {
        const int MOD = 1000000007;
        int n = s.size();
        vector<vector<int>> dp(n, vector<int>(n, 0));
        for (int i = 0; i < n; ++i) dp[i][i] = 1;
        for (int len = 2; len <= n; ++len) {
            for (int i = 0; i <= n - len; ++i) {
                int j = i + len - 1;
                if (s[i] != s[j]) {
                    dp[i][j] = ((dp[i+1][j] + dp[i][j-1]) % MOD - dp[i+1][j-1]) % MOD;
                } else {
                    int low = i + 1, high = j - 1;
                    while (low <= high && s[low] != s[i]) ++low;
                    while (low <= high && s[high] != s[j]) --high;
                    if (low > high) {
                        dp[i][j] = (2 * dp[i+1][j-1] + 2) % MOD;
                    } else if (low == high) {
                        dp[i][j] = (2 * dp[i+1][j-1] + 1) % MOD;
                    } else {
                        dp[i][j] = ((2 * dp[i+1][j-1]) % MOD - dp[low+1][high-1]) % MOD;
                    }
                }
                if (dp[i][j] < 0) dp[i][j] += MOD;
            }
        }
        return dp[0][n-1];
    }
};
      
class Solution {
    public int countPalindromicSubsequences(String s) {
        int MOD = 1000000007;
        int n = s.length();
        int[][] dp = new int[n][n];
        for (int i = 0; i < n; ++i) dp[i][i] = 1;
        for (int len = 2; len <= n; ++len) {
            for (int i = 0; i <= n - len; ++i) {
                int j = i + len - 1;
                if (s.charAt(i) != s.charAt(j)) {
                    dp[i][j] = ((dp[i+1][j] + dp[i][j-1]) % MOD - dp[i+1][j-1]) % MOD;
                } else {
                    int low = i + 1, high = j - 1;
                    while (low <= high && s.charAt(low) != s.charAt(i)) low++;
                    while (low <= high && s.charAt(high) != s.charAt(j)) high--;
                    if (low > high) {
                        dp[i][j] = (2 * dp[i+1][j-1] + 2) % MOD;
                    } else if (low == high) {
                        dp[i][j] = (2 * dp[i+1][j-1] + 1) % MOD;
                    } else {
                        dp[i][j] = ((2 * dp[i+1][j-1]) % MOD - dp[low+1][high-1]) % MOD;
                    }
                }
                if (dp[i][j] < 0) dp[i][j] += MOD;
            }
        }
        return dp[0][n-1];
    }
}
      
var countPalindromicSubsequences = function(s) {
    const MOD = 1e9 + 7;
    const n = s.length;
    const dp = Array.from({length: n}, () => Array(n).fill(0));

    for (let i = 0; i < n; ++i) dp[i][i] = 1;

    for (let len = 2; len <= n; ++len) {
        for (let i = 0; i <= n - len; ++i) {
            let j = i + len - 1;
            if (s[i] !== s[j]) {
                dp[i][j] = ((dp[i+1][j] + dp[i][j-1]) % MOD - dp[i+1][j-1]) % MOD;
            } else {
                let low = i + 1, high = j - 1;
                while (low <= high && s[low] !== s[i]) low++;
                while (low <= high && s[high] !== s[j]) high--;
                if (low > high) {
                    dp[i][j] = (2 * dp[i+1][j-1] + 2) % MOD;
                } else if (low === high) {
                    dp[i][j] = (2 * dp[i+1][j-1] + 1) % MOD;
                } else {
                    dp[i][j] = ((2 * dp[i+1][j-1]) % MOD - dp[low+1][high-1]) % MOD;
                }
            }
            if (dp[i][j] < 0) dp[i][j] += MOD;
        }
    }
    return dp[0][n-1];
};