Facebook Pixel

1147. Longest Chunked Palindrome Decomposition

HardGreedyTwo PointersStringDynamic ProgrammingHash FunctionRolling Hash
Leetcode Link

Problem Description

You are given a string text that needs to be split into k substrings (subtext₁, subtext₂, ..., textₖ) with the following conditions:

  1. Each substring must be non-empty
  2. When concatenated together, all substrings must equal the original text (i.e., subtext₁ + subtext₂ + ... + subtextₖ == text)
  3. The substrings must form a palindromic pattern where subtextᵢ == subtextₖ₋ᵢ₊₁ for all valid values of i (where 1 ≤ i ≤ k)

The third condition means that the first substring must equal the last substring, the second must equal the second-to-last, and so on. For example, if we split "ghiabcdefhelloadamhelloabcdefghi" into 5 parts: ["ghi", "abcdef", "hello", "adamhello", "abcdef", "ghi"], we can see that:

  • 1st substring "ghi" equals 5th substring "ghi"
  • 2nd substring "abcdef" equals 4th substring "abcdef"
  • 3rd substring "helloadamhello" is in the middle (equals itself when k is odd)

Your task is to find the maximum possible value of k - the largest number of substrings you can split the text into while satisfying all three conditions.

The solution uses a greedy two-pointer approach, starting from both ends of the string and looking for the shortest matching prefix and suffix. When a match is found, it counts as 2 pieces (the prefix and suffix), and the process continues with the remaining middle portion. If no match can be found, the remaining string counts as 1 piece.

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

Intuition

The key insight is that we want to maximize the number of pieces, which means we should try to create as many matching pairs as possible from the ends of the string.

Think about it this way: if we have a string and we can find a prefix that matches a suffix, we've created 2 pieces. The question becomes - should we take the shortest matching pair or look for longer ones?

Let's consider why choosing the shortest matching prefix-suffix pair is optimal:

Imagine we have two options:

  • Option A: Take a short matching pair (like "a" from both ends)
  • Option B: Take a longer matching pair (like "abc" from both ends)

If we choose the longer pair (Option B), we're consuming more characters, leaving less room for potential matches in the middle. By choosing the shorter pair (Option A), we leave more characters in the middle, giving us more opportunities to find additional matching pairs.

Here's a visual example: Consider the string "abcXYZabc"

  • If we greedily take "a" and "c" as our first matching pair (both single characters 'a'), we're left with "bcXYZab"
  • If we instead took "abc" as our matching pair, we're left with just "XYZ"

The greedy choice of shorter matches gives us more flexibility for the remaining string.

This naturally leads to a two-pointer approach:

  1. Start with pointers at the beginning and end of the string
  2. Try to find the shortest substring from the left that matches a substring from the right
  3. When found, count it as 2 pieces and move both pointers inward
  4. Repeat until the pointers meet or cross
  5. If there's a remaining middle portion that couldn't be matched, it counts as 1 piece

The correctness of this greedy approach stems from the fact that any valid decomposition can be rearranged to have shorter matches on the outside without reducing the total number of pieces.

Learn more about Greedy, Two Pointers and Dynamic Programming patterns.

Solution Approach

The implementation uses a greedy approach with two pointers to find the maximum number of palindromic chunks.

We maintain two pointers:

  • i starting from the beginning of the string (index 0)
  • j starting from the end of the string (index len(text) - 1)

The algorithm works as follows:

  1. Initialize variables:

    • ans = 0 to count the number of chunks
    • i = 0 and j = len(text) - 1 as our two pointers
  2. Main loop - while i <= j:

    • Set k = 1 to represent the current substring length we're checking
    • Set ok = False to track whether we found a matching pair
  3. Inner loop - search for the shortest matching prefix and suffix:

    • While i + k - 1 < j - k + 1 (ensuring substrings don't overlap):
      • Check if text[i : i + k] == text[j - k + 1 : j + 1]
      • If they match:
        • Add 2 to the answer (we found a matching pair)
        • Move i forward by k positions: i += k
        • Move j backward by k positions: j -= k
        • Set ok = True and break from the inner loop
      • If no match, increment k to try a longer substring
  4. Handle the middle portion:

    • If ok is still False after the inner loop (no matching pair found):
      • Add 1 to the answer (the remaining middle portion becomes a single chunk)
      • Break from the main loop
  5. Return the final answer

Example walkthrough with "ghiabcdefhelloadamhelloabcdefghi":

  • First iteration: Find "ghi" matches at both ends, ans += 2, move pointers inward
  • Second iteration: Find "abcdef" matches at both ends, ans += 2, move pointers inward
  • Third iteration: Remaining string "helloadamhello" has no matching prefix-suffix, ans += 1
  • Final answer: 5

The time complexity is O(n²) in the worst case, where we might need to check multiple substring lengths at each step. The space complexity is O(n) for the string slicing operations.

Ready to land your dream job?

Unlock your dream job with a 5-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through the algorithm with the string "abcab" to find the maximum number of palindromic substrings.

Initial Setup:

  • String: "abcab"
  • i = 0 (pointing to 'a' at the start)
  • j = 4 (pointing to 'b' at the end)
  • ans = 0

Iteration 1:

  • We're looking for matching prefix and suffix
  • Try k = 1: Check if text[0:1] ("a") equals text[4:5] ("b")
    • "a" ≠ "b", no match
  • Try k = 2: Check if text[0:2] ("ab") equals text[3:5] ("ab")
    • "ab" = "ab", match found!
  • Add 2 to answer: ans = 2
  • Move pointers: i = 2, j = 2

Iteration 2:

  • Now i = 2 and j = 2 (both pointing to the middle 'c')
  • Since i <= j is still true, we continue
  • The remaining substring is just "c"
  • Try k = 1: But i + k - 1 = 2 is not less than j - k + 1 = 2
  • No matching pair can be found (we're at a single character)
  • Add 1 to answer: ans = 3
  • Break from loop

Result: The string "abcab" can be split into maximum 3 palindromic substrings: ["ab", "c", "ab"]

Let's verify:

  • Concatenation: "ab" + "c" + "ab" = "abcab" ✓
  • Palindromic pattern: 1st substring "ab" = 3rd substring "ab", 2nd substring "c" = itself ✓

This example demonstrates how the greedy approach finds the shortest matching pairs ("ab" instead of trying longer substrings first) to maximize the total number of pieces.

Solution Implementation

1class Solution:
2    def longestDecomposition(self, text: str) -> int:
3        """
4        Find the maximum number of chunks in a decomposition where the string
5        can be split into (a1, a2, ..., ak, r, ak, ..., a2, a1) where r can be empty.
6      
7        Args:
8            text: The input string to decompose
9          
10        Returns:
11            The maximum number of chunks in the decomposition
12        """
13        chunk_count = 0
14        left_pointer = 0
15        right_pointer = len(text) - 1
16      
17        # Process the string from both ends towards the center
18        while left_pointer <= right_pointer:
19            substring_length = 1
20            match_found = False
21          
22            # Try to find matching substrings of increasing length
23            while left_pointer + substring_length - 1 < right_pointer - substring_length + 1:
24                # Extract substring from left side
25                left_substring = text[left_pointer:left_pointer + substring_length]
26                # Extract substring from right side
27                right_substring = text[right_pointer - substring_length + 1:right_pointer + 1]
28              
29                # Check if the substrings match
30                if left_substring == right_substring:
31                    # Found matching chunks, count them as 2 pieces
32                    chunk_count += 2
33                    # Move pointers inward by the length of matched substring
34                    left_pointer += substring_length
35                    right_pointer -= substring_length
36                    match_found = True
37                    break
38              
39                # Try longer substring in next iteration
40                substring_length += 1
41          
42            # If no matching substrings found, the remaining middle part counts as 1 chunk
43            if not match_found:
44                chunk_count += 1
45                break
46      
47        return chunk_count
48
1class Solution {
2    /**
3     * Finds the maximum number of non-overlapping palindromic substrings
4     * that can be formed by decomposing the given text.
5     * The decomposition works by finding matching prefixes and suffixes.
6     * 
7     * @param text the input string to decompose
8     * @return the maximum number of substrings in the decomposition
9     */
10    public int longestDecomposition(String text) {
11        int decompositionCount = 0;
12        int leftPointer = 0;
13        int rightPointer = text.length() - 1;
14      
15        // Process the string from both ends towards the middle
16        while (leftPointer <= rightPointer) {
17            boolean matchFound = false;
18          
19            // Try to find matching substrings of increasing length
20            for (int substringLength = 1; 
21                 leftPointer + substringLength - 1 < rightPointer - substringLength + 1; 
22                 substringLength++) {
23              
24                // Check if the substring starting at leftPointer matches
25                // the substring starting at (rightPointer - substringLength + 1)
26                if (areSubstringsEqual(text, leftPointer, rightPointer - substringLength + 1, substringLength)) {
27                    // Found matching prefix and suffix
28                    decompositionCount += 2;  // Count both prefix and suffix
29                    leftPointer += substringLength;
30                    rightPointer -= substringLength;
31                    matchFound = true;
32                    break;
33                }
34            }
35          
36            // If no matching prefix and suffix found, the remaining middle part
37            // forms a single substring in the decomposition
38            if (!matchFound) {
39                decompositionCount++;
40                break;
41            }
42        }
43      
44        return decompositionCount;
45    }
46  
47    /**
48     * Checks if two substrings of given length are equal.
49     * 
50     * @param text the string containing the substrings
51     * @param startIndex1 starting index of the first substring
52     * @param startIndex2 starting index of the second substring
53     * @param length the length of substrings to compare
54     * @return true if the substrings are equal, false otherwise
55     */
56    private boolean areSubstringsEqual(String text, int startIndex1, int startIndex2, int length) {
57        // Compare characters one by one
58        for (int offset = 0; offset < length; offset++) {
59            if (text.charAt(startIndex1 + offset) != text.charAt(startIndex2 + offset)) {
60                return false;
61            }
62        }
63        return true;
64    }
65}
66
1class Solution {
2public:
3    int longestDecomposition(string text) {
4        int decompositionCount = 0;
5      
6        // Lambda function to check if two substrings of length 'length' are equal
7        // Starting from positions 'leftStart' and 'rightStart'
8        auto areSubstringsEqual = [&](int leftStart, int rightStart, int length) -> bool {
9            for (int offset = 0; offset < length; ++offset) {
10                if (text[leftStart + offset] != text[rightStart + offset]) {
11                    return false;
12                }
13            }
14            return true;
15        };
16      
17        // Use two pointers approach from both ends of the string
18        int leftPointer = 0;
19        int rightPointer = text.size() - 1;
20      
21        while (leftPointer <= rightPointer) {
22            bool matchFound = false;
23          
24            // Try to find matching substrings of increasing length
25            // Starting from length 1, 2, 3, ...
26            for (int substringLength = 1; 
27                 leftPointer + substringLength - 1 < rightPointer - substringLength + 1; 
28                 ++substringLength) {
29              
30                // Check if the left substring matches the right substring
31                // Left substring: [leftPointer, leftPointer + substringLength - 1]
32                // Right substring: [rightPointer - substringLength + 1, rightPointer]
33                if (areSubstringsEqual(leftPointer, rightPointer - substringLength + 1, substringLength)) {
34                    // Found matching substrings, add 2 to the count (one for left, one for right)
35                    decompositionCount += 2;
36                  
37                    // Move pointers inward by the length of matched substrings
38                    leftPointer += substringLength;
39                    rightPointer -= substringLength;
40                  
41                    matchFound = true;
42                    break;
43                }
44            }
45          
46            // If no matching substrings found, the remaining middle part counts as 1
47            if (!matchFound) {
48                decompositionCount += 1;
49                break;
50            }
51        }
52      
53        return decompositionCount;
54    }
55};
56
1/**
2 * Finds the maximum number of non-overlapping substrings in a palindromic decomposition
3 * where matching substrings are taken from both ends of the text
4 * @param text - The input string to decompose
5 * @returns The maximum number of substrings in the decomposition
6 */
7function longestDecomposition(text: string): number {
8    let resultCount: number = 0;
9    let leftIndex: number = 0;
10    let rightIndex: number = text.length - 1;
11  
12    // Process the string from both ends towards the center
13    while (leftIndex <= rightIndex) {
14        let matchFound: boolean = false;
15      
16        // Try to find matching substrings of increasing length
17        for (let substringLength: number = 1; 
18             leftIndex + substringLength - 1 < rightIndex - substringLength + 1; 
19             substringLength++) {
20          
21            // Extract substring from left side
22            const leftSubstring: string = text.slice(leftIndex, leftIndex + substringLength);
23            // Extract substring from right side
24            const rightSubstring: string = text.slice(rightIndex - substringLength + 1, rightIndex + 1);
25          
26            // Check if the substrings match
27            if (leftSubstring === rightSubstring) {
28                // Found matching pair, increment count by 2
29                resultCount += 2;
30                // Move left pointer forward
31                leftIndex += substringLength;
32                // Move right pointer backward
33                rightIndex -= substringLength;
34                matchFound = true;
35                break;
36            }
37        }
38      
39        // If no matching pair found, we have a middle element
40        if (!matchFound) {
41            // Add 1 for the remaining middle portion
42            resultCount++;
43            break;
44        }
45    }
46  
47    return resultCount;
48}
49

Time and Space Complexity

Time Complexity: O(n²)

The algorithm uses two pointers i and j starting from both ends of the string. In the worst case:

  • The outer while loop runs until i <= j, which can iterate up to n/2 times where n is the length of the string
  • For each iteration of the outer loop, the inner while loop increments k to find matching substrings
  • In the worst case, k can grow up to n/2 (when no matching prefix-suffix pairs are found until the middle)
  • String slicing text[i : i + k] and text[j - k + 1 : j + 1] takes O(k) time
  • String comparison also takes O(k) time

Therefore, the worst-case scenario involves O(n/2) iterations of the outer loop, with each iteration potentially doing O(n/2) work in the inner loop for substring operations, resulting in O(n²) time complexity.

Space Complexity: O(n)

The space complexity is O(n) due to the string slicing operations. When Python performs slicing like text[i : i + k] and text[j - k + 1 : j + 1], it creates new string objects that require additional memory proportional to the slice length. In the worst case, these slices can be up to O(n/2) in length.

If the implementation were modified to compare characters directly without creating substring copies (using index-based character comparison), the space complexity could be reduced to O(1).

Learn more about how to find time and space complexity quickly.

Common Pitfalls

Pitfall 1: Incorrect Handling of the Middle Element

The Problem: A common mistake is forgetting to count the middle portion when no matching prefix-suffix pair can be found. When the pointers meet or cross without finding any matches, the remaining string still counts as one valid chunk.

Incorrect Implementation:

while left_pointer <= right_pointer:
    substring_length = 1
    match_found = False
  
    while left_pointer + substring_length - 1 < right_pointer - substring_length + 1:
        if left_substring == right_substring:
            chunk_count += 2
            match_found = True
            break
        substring_length += 1
  
    # WRONG: Forgetting to handle the case when no match is found
    if not match_found:
        break  # This loses the middle chunk!

Correct Implementation:

if not match_found:
    chunk_count += 1  # Count the remaining middle portion
    break

Pitfall 2: Off-by-One Errors in Pointer Boundaries

The Problem: The condition for checking substring overlap is critical. Using <= instead of < in the inner loop condition can cause the algorithm to check overlapping substrings, leading to incorrect results.

Incorrect Implementation:

# WRONG: This allows overlapping substrings
while left_pointer + substring_length - 1 <= right_pointer - substring_length + 1:
    # This might check overlapping regions

Correct Implementation:

# Ensures substrings don't overlap
while left_pointer + substring_length - 1 < right_pointer - substring_length + 1:
    # Proper boundary checking

Pitfall 3: Greedy Algorithm Correctness Assumption

The Problem: Some might think we need to try all possible decompositions to find the maximum k, not trusting that the greedy approach (finding shortest matching prefix-suffix) always yields the optimal solution.

Why Greedy Works: The key insight is that if we have a matching prefix and suffix of length L, splitting them off immediately is always optimal. Any alternative decomposition that keeps these L characters as part of larger chunks would result in fewer total chunks. The greedy choice of smallest matching chunks maximizes the number of pieces.

Example to Illustrate: For string "abcabc":

  • Greedy approach: Split as ["a", "b", "c", "a", "b", "c"] → 6 chunks (though this would actually be ["a","bc","a"] → 3 chunks following the algorithm)
  • Non-greedy might try: ["abc", "abc"] → 2 chunks
  • The greedy approach correctly identifies ["a", "bc", "a"] by finding the shortest match first

Pitfall 4: String Slicing Performance

The Problem: Creating new substring copies with slicing operations in Python can be inefficient for very long strings, potentially causing performance issues.

Alternative Approach Using Indices:

def longestDecomposition(self, text: str) -> int:
    def matches(start1, end1, start2, end2):
        # Compare characters without creating substrings
        if end1 - start1 != end2 - start2:
            return False
        for i in range(end1 - start1):
            if text[start1 + i] != text[start2 + i]:
                return False
        return True
  
    chunk_count = 0
    left = 0
    right = len(text) - 1
  
    while left <= right:
        length = 1
        found = False
      
        while left + length - 1 < right - length + 1:
            if matches(left, left + length, right - length + 1, right + 1):
                chunk_count += 2
                left += length
                right -= length
                found = True
                break
            length += 1
      
        if not found:
            chunk_count += 1
            break
  
    return chunk_count

This approach avoids creating substring copies and can be more memory-efficient for large inputs.

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

Which of the following problems can be solved with backtracking (select multiple)


Recommended Readings

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

Load More