Facebook Pixel

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:

  1. Each substring must have a length of at least k characters
  2. 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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. Skip position i and find the best solution starting from position i+1
  2. Choose a palindrome starting at position i (if one exists with length at least k), 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 to 0)
  • For each starting position i, we check all ending positions j > i
  • A substring s[i...j] is a palindrome if: s[i] == s[j] and dp[i+1][j-1] is True

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, return 0 (no more characters to process)
  • We have two choices at each position:
    1. Skip the current position and solve for dfs(i + 1)
    2. Choose a palindrome starting at position i with length at least k

For the second choice, we iterate through all possible ending positions j where:

  • j >= i + k - 1 (ensuring the palindrome has at least k 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 Evaluator

Example 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', so dp[7][8] = False

For position 6 (character 'b'):

  • Check s[6...7]: "bb" → 'b' = 'b', so dp[6][7] = True
  • Check s[6...8]: "bbd" → 'b' ≠ 'd', so dp[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:

  1. Precomputes all palindromes efficiently
  2. Uses memoized recursion to avoid overlapping selections
  3. 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) where i < j, which takes O(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 from i + k - 1 to n - 1, which is at most O(n) operations. Therefore, the DFS contributes O(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, requiring O(n^2) space.
  • The memoization cache for the DFS function stores at most n states, each taking O(1) space, contributing O(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] when i+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².

Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

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

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More