Topics › Strings › Hard String Problems
Count Palindromic Subsequences
Java source ↗ Edit this article ↗
Jump to code:
Problem
Given a string s of length n, count its non-empty subsequences that are palindromes. A subsequence is obtained by deleting zero or more characters without reordering the rest. Subsequences are identified by the positions they use, so two equal strings taken from different positions are counted separately. Return the count modulo 10^9 + 7.
Constraints: 1 ≤ n ≤ 1000 (the Java file also accepts the empty string and returns 0).
Example 1
s = "abcd"
answer = 4 // only the single letters a, b, c, d
Example 2
s = "aab"
answer = 4 // a (position 0), a (position 1), b, aa
Example 3
s = "abcb"
answer = 6 // a, b, c, b, bb, bcb
This is the “count all, not necessarily distinct” version. Counting only distinct palindromic subsequences is a different and harder problem.
Approach 1: Brute force
Intuition. Each position is either taken or skipped, so a string of length n has 2^n - 1 non-empty subsequences. Generate each one from a bit mask, test whether it reads the same backwards, and count. It is hopeless beyond about 20 characters, but it is the definition turned into code, which makes it the right reference for testing.
Algorithm.
- For every mask from
1to2^n - 1, build the subsequence of positions whose bits are set. - If it equals its reverse, increment the count.
- Return the count modulo
10^9 + 7.
static int bruteForce(String s) {
int n = s.length();
if (n > 20) throw new IllegalArgumentException("brute force is for tiny inputs only");
long count = 0;
for (int mask = 1; mask < (1 << n); mask++) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < n; i++) {
if ((mask >> i & 1) == 1) sb.append(s.charAt(i));
}
String t = sb.toString();
if (t.equals(sb.reverse().toString())) count++;
}
return (int) (count % MOD);
}
Complexity. Time O(2^n · n). Space O(n) for the subsequence being built.
Building the recurrence
Let C(i, j) be the number of palindromic subsequences that use only positions i..j. Split them by whether they use the two end positions.
- Those that do not use position
iare counted byC(i+1, j). - Those that do not use position
jare counted byC(i, j-1). - Those using neither end are in both groups, counted by
C(i+1, j-1). By inclusion-exclusion, the subsequences that avoid at least one end numberC(i+1, j) + C(i, j-1) - C(i+1, j-1). - Those that use both ends start with
s[i]and end withs[j]. Ifs[i] != s[j]there are none. Ifs[i] == s[j], each one iss[i], then any palindrome strictly inside or nothing at all, thens[j]. That givesC(i+1, j-1) + 1of them.
Adding the cases, the C(i+1, j-1) terms cancel when the ends match:
s[i] == s[j]: C(i, j) = C(i+1, j) + C(i, j-1) + 1
s[i] != s[j]: C(i, j) = C(i+1, j) + C(i, j-1) - C(i+1, j-1)
base cases: C(i, i) = 1, C(i, j) = 0 when i > j
The answer is C(0, n-1). There are only n^2 / 2 distinct (i, j) pairs, but plain recursion would revisit them exponentially often, which is why we store results.
Approach 2: Better, memoized recursion
Intuition. Implement the recurrence directly and cache each C(i, j) the first time it is computed. Values are stored already reduced modulo 10^9 + 7. The subtraction in the second case can then produce a negative number, so MOD is added before reducing.
Algorithm.
- Fill a memo table with
-1. count(i, j): return0ifi > j,1ifi == j, the memo if present; otherwise apply the recurrence and store the result.- Return
count(0, n - 1).
static int memoized(String s) {
int n = s.length();
if (n == 0) return 0;
int[][] memo = new int[n][n];
for (int[] row : memo) Arrays.fill(row, -1);
return count(s, 0, n - 1, memo);
}
static int count(String s, int i, int j, int[][] memo) {
if (i > j) return 0;
if (i == j) return 1;
if (memo[i][j] != -1) return memo[i][j];
long res;
if (s.charAt(i) == s.charAt(j)) {
res = (long) count(s, i + 1, j, memo) + count(s, i, j - 1, memo) + 1;
} else {
res = (long) count(s, i + 1, j, memo) + count(s, i, j - 1, memo)
- count(s, i + 1, j - 1, memo) + MOD; // + MOD keeps the value non-negative
}
return memo[i][j] = (int) (res % MOD);
}
Complexity. Time O(n^2): each pair is computed once with O(1) work. Space O(n^2) for the memo plus O(n) recursion depth, since every call shrinks the interval by one.
Approach 3: Optimal, bottom-up tabulation
Intuition. C(i, j) depends only on intervals that start later (i + 1) or end earlier (j - 1). So filling the table with i going down from n - 1 and, for each i, j going up from i + 1, guarantees that every needed value is ready. This removes the recursion and its stack, and it runs faster in practice.
Algorithm.
- For
i = n - 1down to0: setdp[i][i] = 1. - For
j = i + 1 .. n - 1: apply the recurrence, treatingdp[i+1][j-1]as0when the interval is empty. - Return
dp[0][n - 1].
static int tabulation(String s) {
int n = s.length();
if (n == 0) return 0;
int[][] dp = new int[n][n]; // dp[i][j] = answer for s[i..j]
for (int i = n - 1; i >= 0; i--) {
dp[i][i] = 1;
for (int j = i + 1; j < n; j++) {
long inner = (i + 1 <= j - 1) ? dp[i + 1][j - 1] : 0;
long v;
if (s.charAt(i) == s.charAt(j)) v = (long) dp[i + 1][j] + dp[i][j - 1] + 1;
else v = (long) dp[i + 1][j] + dp[i][j - 1] - inner + MOD;
dp[i][j] = (int) (v % MOD);
}
}
return dp[0][n - 1];
}
Dry run on s = "abcb" (row i, column j):
j=0 j=1 j=2 j=3
i=0 1 2 3 6
i=1 1 2 5
i=2 1 2
i=3 1
Two cells in detail. dp[1][3] has equal ends b, b: dp[2][3] + dp[1][2] + 1 = 2 + 2 + 1 = 5, which is b, c, b, bb, bcb. dp[0][3] has ends a, b: dp[1][3] + dp[0][2] - dp[1][2] = 5 + 3 - 2 = 6.
Complexity. Time O(n^2), about 5 · 10^5 cell updates for n = 1000. Space O(n^2). Row i reads only row i + 1 and itself, so two one-dimensional arrays of length n would reduce the space to O(n) if memory were tight.
Edge cases and pitfalls
- Modular subtraction.
(a + b - c) % MODcan be negative in Java. AddMODfirst, as the code does; witha, b, c < MOD, the suma + b - c + MODis always positive. - Overflow before the modulus. Two values below
10^9 + 7add up to more thanInteger.MAX_VALUE. Do the arithmetic inlong. - Counting by position.
"aa"has three palindromic subsequences: two singleas andaa. A string ofnequal letters has2^n - 1, which the Java file checks forn = 1000. - Empty subsequence. It is not counted. The
+ 1in the equal-ends case counts the two-letter palindromes[i] s[j], not the empty string. - Distinct counting is different. Do not de-duplicate unless the problem asks for distinct palindromes; that variant needs a more involved recurrence.
Related problems
- Longest palindromic subsequence uses the same interval DP on
(i, j)with a max instead of a count. - Palindromic Substrings counts contiguous palindromes instead.
- Longest Palindromic Substring finds the longest contiguous palindrome.
- Learn All Patterns of Subsequences (Theory) covers the take / not-take enumeration behind the brute force.
- Count all subsequences with sum K is another subsequence-counting problem.