2472. Maximum Number of Non-overlapping Palindrome Substrings
Problem Description
You are given a string s
and a positive integer k
. Your task is to select a set of non-overlapping substrings from the string that meet specific criteria and maximize the count of such substrings.
The selected substrings must satisfy two conditions:
- Each substring must have a length of at least
k
characters - Each substring must be a palindrome (reads the same forwards and backwards)
The substrings you select cannot overlap with each other - meaning they cannot share any character positions in the original string.
Your goal is to find the maximum number of such palindromic substrings you can select from the string s
.
For example, if you have a string like "abaccdbbd" and k = 3
, you could potentially select palindromes like "aba" and "bdb" (which don't overlap), giving you a count of 2. The challenge is to find the optimal selection that maximizes this count.
A substring is defined as a contiguous sequence of characters within the string, meaning the characters must appear consecutively in the original string.
Intuition
To solve this problem, we need to think about two main challenges: identifying all possible palindromes and then selecting the maximum number of non-overlapping ones.
The first insight is that we need to know which substrings are palindromes before we can make any selection decisions. Checking if a substring is a palindrome repeatedly would be inefficient, so we can precompute this information. We can use dynamic programming to build a 2D table dp[i][j]
that tells us whether the substring from index i
to index j
is a palindrome. The key observation here is that a substring s[i...j]
is a palindrome if and only if s[i] == s[j]
and the inner substring s[i+1...j-1]
is also a palindrome.
Once we know all the palindromes, the second challenge is selecting the maximum number of non-overlapping ones. This is where the problem becomes interesting - it's essentially an optimization problem where we need to make choices. At each position in the string, we have a decision to make: should we include a palindrome starting at this position, or skip it and look for palindromes starting later?
This decision-making process naturally leads to a recursive approach. For any position i
in the string, we can either:
- Skip position
i
and find the best solution starting from positioni+1
- Choose a palindrome starting at position
i
(if one exists with length at leastk
), and then find the best solution starting after that palindrome ends
The key insight is that once we choose a palindrome from position i
to position j
, the next palindrome can only start from position j+1
or later (to avoid overlapping). This creates a natural recursive structure where each decision leads to a smaller subproblem.
By using memoization with our recursive function dfs(i)
, we avoid recalculating the same subproblems multiple times. The function dfs(i)
represents the maximum number of palindromes we can select starting from position i
onwards, considering all the constraints.
Learn more about Greedy, Two Pointers and Dynamic Programming patterns.
Solution Approach
The solution consists of two main phases: preprocessing to identify all palindromes and a memoized recursive search to find the maximum number of non-overlapping palindromes.
Phase 1: Preprocessing Palindromes
We create a 2D boolean table dp[i][j]
where dp[i][j] = True
if the substring s[i...j]
is a palindrome. We build this table using dynamic programming:
- Initialize all single characters as palindromes:
dp[i][i] = True
- For each substring, we check from the end of the string backwards (starting from
i = n-1
down to0
) - For each starting position
i
, we check all ending positionsj > i
- A substring
s[i...j]
is a palindrome if:s[i] == s[j]
anddp[i+1][j-1]
isTrue
This preprocessing takes O(n^2)
time and space, where n
is the length of the string.
Phase 2: Memoized Recursive Search
We define a recursive function dfs(i)
that returns the maximum number of non-overlapping palindromes that can be selected from s[i...]
(the substring starting at position i
).
The recurrence relation is:
dfs(i) = max( dfs(i + 1), // Skip position i max{1 + dfs(j + 1) for all j where j >= i + k - 1 and dp[i][j] is True} )
Breaking this down:
- Base case: If
i >= n
, return0
(no more characters to process) - We have two choices at each position:
- Skip the current position and solve for
dfs(i + 1)
- Choose a palindrome starting at position
i
with length at leastk
- Skip the current position and solve for
For the second choice, we iterate through all possible ending positions j
where:
j >= i + k - 1
(ensuring the palindrome has at leastk
characters)dp[i][j] = True
(ensuring the substring is actually a palindrome)
If we choose the palindrome from i
to j
, we add 1
to our count and continue searching from position j + 1
(the first position after our chosen palindrome).
The @cache
decorator (memoization) ensures that each unique state dfs(i)
is computed only once, reducing the time complexity from exponential to O(n^2)
.
The final answer is dfs(0)
, which gives us the maximum number of non-overlapping palindromes starting from the beginning of the string.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through the solution with the string s = "abaccdbbd"
and k = 3
.
Phase 1: Building the Palindrome Table
First, we build our dp
table to identify all palindromes. We'll create a 9×9 table (string length is 9).
Starting with single characters (all are palindromes):
dp[0][0] = True
('a')dp[1][1] = True
('b')- ... and so on for all positions
Then we check longer substrings, working backwards from position 8 to 0:
For position 7 (character 'b'):
- Check
s[7...8]
: "bd" → 'b' ≠ 'd', sodp[7][8] = False
For position 6 (character 'b'):
- Check
s[6...7]
: "bb" → 'b' = 'b', sodp[6][7] = True
- Check
s[6...8]
: "bbd" → 'b' ≠ 'd', sodp[6][8] = False
Continuing this process, we identify key palindromes of length ≥ 3:
dp[0][2] = True
: "aba" (positions 0-2)dp[2][4] = True
: "acc" (positions 2-4) - Wait, this is False ('a' ≠ 'c')dp[3][5] = True
: "ccd" - False ('c' ≠ 'd')dp[5][7] = True
: "dbb" - False ('d' ≠ 'b')dp[6][8] = True
: "bbd" - False ('b' ≠ 'd')
Actually, let me recalculate the palindromes correctly:
- "aba" (0-2): 'a' = 'a' and 'b' is palindrome →
dp[0][2] = True
- "bac" (1-3): 'b' ≠ 'c' →
dp[1][3] = False
- "acc" (2-4): 'a' ≠ 'c' →
dp[2][4] = False
- "ccd" (3-5): 'c' ≠ 'd' →
dp[3][5] = False
- "cdb" (4-6): 'c' ≠ 'b' →
dp[4][6] = False
- "dbb" (5-7): 'd' ≠ 'b' →
dp[5][7] = False
- "bbd" (6-8): 'b' ≠ 'd' →
dp[6][8] = False
Let's check for "bdb" (positions 1,5,6): This isn't contiguous! We need contiguous substrings.
Actually, looking more carefully at "abaccdbbd":
- Position 5-7: "dbb" → 'd' ≠ 'b', not a palindrome
- Position 4-6: "cdb" → 'c' ≠ 'b', not a palindrome
The valid palindromes of length ≥ 3 are:
- "aba" (positions 0-2):
dp[0][2] = True
Phase 2: Finding Maximum Non-overlapping Palindromes
Now we use our recursive function dfs(i)
to find the maximum count:
dfs(0)
: Starting from position 0
- Option 1: Skip position 0 →
dfs(1)
- Option 2: Take palindrome "aba" (0-2) →
1 + dfs(3)
Let's evaluate dfs(1)
: Starting from position 1
- No palindromes of length ≥ 3 start at position 1
- Return
dfs(2)
dfs(2)
: Starting from position 2
- No palindromes of length ≥ 3 start at position 2
- Continue similarly...
dfs(3)
: Starting from position 3
- No palindromes of length ≥ 3 start here
- Continue checking...
Since we don't find any other palindromes of length ≥ 3 in the remaining string, most dfs
calls return 0.
Therefore:
dfs(3)
= 0 (no more palindromes found)dfs(0)
= max(dfs(1), 1 + dfs(3)) = max(0, 1 + 0) = 1
The maximum number of non-overlapping palindromes of length at least 3 is 1 (just "aba").
This example shows how the algorithm:
- Precomputes all palindromes efficiently
- Uses memoized recursion to avoid overlapping selections
- Maximizes the count by trying all valid combinations
Solution Implementation
1class Solution:
2 def maxPalindromes(self, s: str, k: int) -> int:
3 """
4 Find the maximum number of non-overlapping palindromic substrings of length at least k.
5
6 Args:
7 s: Input string
8 k: Minimum length of palindromic substrings
9
10 Returns:
11 Maximum number of non-overlapping palindromic substrings
12 """
13 from functools import cache
14
15 @cache
16 def find_max_palindromes(start_index: int) -> int:
17 """
18 Dynamic programming function to find maximum palindromes starting from index.
19
20 Args:
21 start_index: Current starting position in the string
22
23 Returns:
24 Maximum number of palindromes from this position
25 """
26 # Base case: reached end of string
27 if start_index >= string_length:
28 return 0
29
30 # Option 1: Skip current position and check from next index
31 max_count = find_max_palindromes(start_index + 1)
32
33 # Option 2: Try to include palindromes starting at current position
34 # Check all possible ending positions that form palindromes of length >= k
35 for end_index in range(start_index + k - 1, string_length):
36 if is_palindrome[start_index][end_index]:
37 # Include this palindrome and continue from position after it
38 max_count = max(max_count, 1 + find_max_palindromes(end_index + 1))
39
40 return max_count
41
42 string_length = len(s)
43
44 # Build 2D DP table to check if substring s[i:j+1] is a palindrome
45 # is_palindrome[i][j] = True if s[i:j+1] is a palindrome
46 is_palindrome = [[True] * string_length for _ in range(string_length)]
47
48 # Fill the palindrome table bottom-up
49 # Start from the end of string and work backwards
50 for start in range(string_length - 1, -1, -1):
51 for end in range(start + 1, string_length):
52 # A substring is palindrome if:
53 # 1. First and last characters match
54 # 2. Inner substring is also a palindrome (or length <= 2)
55 is_palindrome[start][end] = (s[start] == s[end] and
56 is_palindrome[start + 1][end - 1])
57
58 # Find the maximum number of palindromes
59 result = find_max_palindromes(0)
60
61 # Clear the cache to free memory
62 find_max_palindromes.cache_clear()
63
64 return result
65
1class Solution {
2 // Memoization array to store the maximum number of palindromes starting from index i
3 private int[] memo;
4 // 2D array to check if substring s[i..j] is a palindrome
5 private boolean[][] isPalindrome;
6 // Input string
7 private String s;
8 // Length of the string
9 private int n;
10 // Minimum length of palindrome substrings
11 private int k;
12
13 /**
14 * Finds the maximum number of non-overlapping palindromic substrings
15 * with length at least k in the given string.
16 *
17 * @param s the input string
18 * @param k the minimum length of palindrome substrings
19 * @return the maximum number of non-overlapping palindromic substrings
20 */
21 public int maxPalindromes(String s, int k) {
22 this.n = s.length();
23 this.s = s;
24 this.k = k;
25
26 // Initialize memoization array with -1 (unvisited state)
27 this.memo = new int[n];
28 Arrays.fill(memo, -1);
29
30 // Initialize palindrome check array
31 this.isPalindrome = new boolean[n][n];
32
33 // Precompute all palindromic substrings using dynamic programming
34 // All single characters are palindromes
35 for (int i = 0; i < n; i++) {
36 Arrays.fill(isPalindrome[i], true);
37 }
38
39 // Build palindrome table from bottom to top
40 // For each starting position i (from right to left)
41 for (int i = n - 1; i >= 0; i--) {
42 // For each ending position j (from i+1 to right)
43 for (int j = i + 1; j < n; j++) {
44 // A substring is palindrome if:
45 // 1. First and last characters match
46 // 2. The inner substring is also a palindrome
47 isPalindrome[i][j] = (s.charAt(i) == s.charAt(j)) && isPalindrome[i + 1][j - 1];
48 }
49 }
50
51 // Start DFS from index 0 to find maximum palindromes
52 return dfs(0);
53 }
54
55 /**
56 * Depth-first search with memoization to find the maximum number of
57 * non-overlapping palindromic substrings starting from index i.
58 *
59 * @param i the starting index
60 * @return the maximum number of palindromes from index i to the end
61 */
62 private int dfs(int i) {
63 // Base case: reached the end of the string
64 if (i >= n) {
65 return 0;
66 }
67
68 // Return memoized result if already computed
69 if (memo[i] != -1) {
70 return memo[i];
71 }
72
73 // Option 1: Skip current character and continue from next position
74 int maxPalindromes = dfs(i + 1);
75
76 // Option 2: Try to form a palindrome starting at index i
77 // Check all possible ending positions j where substring length >= k
78 for (int j = i + k - 1; j < n; j++) {
79 // If substring s[i..j] is a palindrome
80 if (isPalindrome[i][j]) {
81 // Take this palindrome and continue from j+1
82 maxPalindromes = Math.max(maxPalindromes, 1 + dfs(j + 1));
83 }
84 }
85
86 // Store the result in memoization array
87 memo[i] = maxPalindromes;
88 return maxPalindromes;
89 }
90}
91
1class Solution {
2public:
3 int maxPalindromes(string s, int k) {
4 int n = s.size();
5
6 // isPalindrome[i][j] = true if substring s[i...j] is a palindrome
7 vector<vector<bool>> isPalindrome(n, vector<bool>(n, true));
8
9 // memo[i] stores the maximum number of non-overlapping palindromes
10 // starting from index i, -1 indicates not computed yet
11 vector<int> memo(n, -1);
12
13 // Build palindrome table using dynamic programming
14 // Check all substrings from right to left, bottom to top
15 for (int start = n - 1; start >= 0; --start) {
16 for (int end = start + 1; end < n; ++end) {
17 // A substring is palindrome if first and last characters match
18 // and the inner substring is also a palindrome
19 isPalindrome[start][end] = (s[start] == s[end]) &&
20 isPalindrome[start + 1][end - 1];
21 }
22 }
23
24 // Recursive function with memoization to find maximum palindromes
25 function<int(int)> findMaxPalindromes = [&](int startIdx) -> int {
26 // Base case: reached end of string
27 if (startIdx >= n) {
28 return 0;
29 }
30
31 // Return memoized result if already computed
32 if (memo[startIdx] != -1) {
33 return memo[startIdx];
34 }
35
36 // Option 1: Skip current position and check from next index
37 int maxCount = findMaxPalindromes(startIdx + 1);
38
39 // Option 2: Try to form palindromes starting at current position
40 // Only consider palindromes of length at least k
41 for (int endIdx = startIdx + k - 1; endIdx < n; ++endIdx) {
42 if (isPalindrome[startIdx][endIdx]) {
43 // Include this palindrome and continue from next non-overlapping position
44 maxCount = max(maxCount, 1 + findMaxPalindromes(endIdx + 1));
45 }
46 }
47
48 // Memoize and return result
49 memo[startIdx] = maxCount;
50 return maxCount;
51 };
52
53 // Start finding maximum palindromes from index 0
54 return findMaxPalindromes(0);
55 }
56};
57
1function maxPalindromes(s: string, k: number): number {
2 const n: number = s.length;
3
4 // isPalindrome[i][j] = true if substring s[i...j] is a palindrome
5 const isPalindrome: boolean[][] = Array(n).fill(null).map(() => Array(n).fill(true));
6
7 // memo[i] stores the maximum number of non-overlapping palindromes
8 // starting from index i, -1 indicates not computed yet
9 const memo: number[] = Array(n).fill(-1);
10
11 // Build palindrome table using dynamic programming
12 // Check all substrings from right to left, bottom to top
13 for (let start = n - 1; start >= 0; start--) {
14 for (let end = start + 1; end < n; end++) {
15 // A substring is palindrome if first and last characters match
16 // and the inner substring is also a palindrome
17 isPalindrome[start][end] = (s[start] === s[end]) &&
18 isPalindrome[start + 1][end - 1];
19 }
20 }
21
22 // Recursive function with memoization to find maximum palindromes
23 const findMaxPalindromes = (startIdx: number): number => {
24 // Base case: reached end of string
25 if (startIdx >= n) {
26 return 0;
27 }
28
29 // Return memoized result if already computed
30 if (memo[startIdx] !== -1) {
31 return memo[startIdx];
32 }
33
34 // Option 1: Skip current position and check from next index
35 let maxCount: number = findMaxPalindromes(startIdx + 1);
36
37 // Option 2: Try to form palindromes starting at current position
38 // Only consider palindromes of length at least k
39 for (let endIdx = startIdx + k - 1; endIdx < n; endIdx++) {
40 if (isPalindrome[startIdx][endIdx]) {
41 // Include this palindrome and continue from next non-overlapping position
42 maxCount = Math.max(maxCount, 1 + findMaxPalindromes(endIdx + 1));
43 }
44 }
45
46 // Memoize and return result
47 memo[startIdx] = maxCount;
48 return maxCount;
49 };
50
51 // Start finding maximum palindromes from index 0
52 return findMaxPalindromes(0);
53}
54
Time and Space Complexity
The time complexity is O(n^2)
and the space complexity is O(n^2)
, where n
is the length of the string s
.
Time Complexity Analysis:
- Building the palindrome DP table: The nested loops iterate through all pairs
(i, j)
wherei < j
, which takesO(n^2)
time. - The DFS function with memoization: Each state
i
(from 0 to n-1) is computed at most once due to the@cache
decorator. For each state, we iterate through positions fromi + k - 1
ton - 1
, which is at mostO(n)
operations. Therefore, the DFS contributesO(n) * O(n) = O(n^2)
time. - Overall time complexity:
O(n^2) + O(n^2) = O(n^2)
.
Space Complexity Analysis:
- The 2D DP table
dp[i][j]
stores palindrome information for all substring pairs, requiringO(n^2)
space. - The memoization cache for the DFS function stores at most
n
states, each takingO(1)
space, contributingO(n)
space. - The recursion stack depth is at most
O(n)
in the worst case. - Overall space complexity:
O(n^2) + O(n) + O(n) = O(n^2)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Greedy Selection of Palindromes
The Pitfall: A common mistake is attempting to solve this problem greedily by always selecting the first valid palindrome encountered. For example, given the string "aabaa" with k=2, a greedy approach might select "aa" at position 0-1, then "aa" at position 3-4, yielding 2 palindromes. However, the optimal solution is to select the single palindrome "aabaa" (positions 0-4), then potentially find more palindromes afterward.
Why This Fails: Selecting shorter palindromes early can block the selection of longer palindromes that might lead to better overall solutions. The problem requires global optimization, not local optimization.
Solution: The recursive approach with memoization correctly explores all possibilities - both taking and skipping palindromes at each position - ensuring we find the globally optimal solution.
2. Incorrect Palindrome DP Table Initialization
The Pitfall: When building the palindrome checking table, incorrectly handling the base cases can lead to wrong results. Specifically:
- Forgetting that single characters are palindromes
- Not properly handling adjacent characters (length 2 substrings)
- Accessing
dp[i+1][j-1]
wheni+1 > j-1
, which can give incorrect results
Example of Incorrect Code:
# WRONG: This doesn't handle edge cases properly
for start in range(string_length):
for end in range(start + 1, string_length):
is_palindrome[start][end] = (s[start] == s[end] and
is_palindrome[start + 1][end - 1])
Solution: Initialize the diagonal (single characters) as True and process the table in the correct order (bottom-up from the end of the string):
# Initialize all positions as True (handles single characters and edge cases)
is_palindrome = [[True] * string_length for _ in range(string_length)]
# Process from end to beginning to ensure dp[i+1][j-1] is computed before dp[i][j]
for start in range(string_length - 1, -1, -1):
for end in range(start + 1, string_length):
is_palindrome[start][end] = (s[start] == s[end] and
is_palindrome[start + 1][end - 1])
3. Off-by-One Errors in Index Handling
The Pitfall: Confusion between inclusive and exclusive indices when:
- Calculating substring length (should be
end - start + 1
) - Checking minimum length constraint (should be
end >= start + k - 1
) - Continuing recursion after selecting a palindrome (should continue from
end + 1
)
Example of Incorrect Code:
# WRONG: Off-by-one error in length check
for end_index in range(start_index + k, string_length): # Should be start_index + k - 1
if is_palindrome[start_index][end_index]:
max_count = max(max_count, 1 + find_max_palindromes(end_index)) # Should be end_index + 1
Solution: Be explicit about index calculations:
# Correct: end_index is inclusive, so length = end - start + 1
# For length >= k, we need end >= start + k - 1
for end_index in range(start_index + k - 1, string_length):
if is_palindrome[start_index][end_index]:
# Continue from the position after the selected palindrome
max_count = max(max_count, 1 + find_max_palindromes(end_index + 1))
4. Memory Optimization Opportunities Missed
The Pitfall: While the solution works correctly, it might use more memory than necessary. The palindrome table uses O(n²) space, but we could potentially optimize this.
Solution: For very large strings, consider using a set to store only valid palindrome ranges instead of a full 2D table:
# Memory-optimized approach for sparse palindromes
valid_palindromes = set()
for start in range(string_length):
for end in range(start + k - 1, string_length):
if is_palindrome_check(s, start, end): # Check palindrome on-the-fly
valid_palindromes.add((start, end))
This is particularly useful when the number of valid palindromes of length ≥ k is much smaller than n².
How would you design a stack which has a function min
that returns the minimum element in the stack, in addition to push
and pop
? All push
, pop
, min
should have running time O(1)
.
Recommended Readings
Greedy Introduction div class responsive iframe iframe src https www youtube com embed WTslqPbj7I title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture web share allowfullscreen iframe div When do we use greedy Greedy algorithms tend to solve optimization problems Typically they will ask you to calculate the max min of some value Commonly you may see this phrased in the problem as max min longest shortest largest smallest etc These keywords can be identified by just scanning
Tech Interview Pattern Two Pointers Introduction If you prefer videos here's a super quick introduction to Two Pointers div class responsive iframe iframe src https www youtube com embed xZ4AfXHQ1VQ title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture allowfullscreen iframe div Two pointers is a common interview
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
Want a Structured Path to Master System Design Too? Don’t Miss This!