LeetCode 1147 - Longest Chunked Palindrome Decomposition
The problem asks us to decompose a given string text into the largest possible number of contiguous substrings such that the sequence of substrings forms a palindromic pattern.
Difficulty: 🔴 Hard
Topics: Two Pointers, String, Dynamic Programming, Greedy, Rolling Hash, Hash Function
Solution
Problem Understanding
The problem asks us to decompose a given string text into the largest possible number of contiguous substrings such that the sequence of substrings forms a palindromic pattern. Specifically, the first substring must equal the last substring, the second substring must equal the second-to-last substring, and so on. The goal is to maximize the number of these chunks, denoted as k.
The input text is a string of length up to 1000 characters, consisting only of lowercase English letters. The output is a single integer representing the maximum number of chunks into which the string can be split while preserving the palindromic chunk property.
The constraints imply that brute-force checking of all possible splits would likely be inefficient. Edge cases include strings that are already palindromic, strings with no repeating patterns, and strings where the maximal decomposition involves many small chunks. The problem guarantees that the string is non-empty, which simplifies handling boundary cases.
Approaches
The brute-force approach would attempt to split the string at every possible prefix and recursively check if the remaining string can also be split into palindromic chunks. While this guarantees correctness, it is highly inefficient because it explores an exponential number of substring combinations, which is infeasible for the upper limit of 1000 characters.
The key insight for an optimal solution is that we do not need to check every possible combination. We can use a two-pointer approach starting from the left and right ends of the string. At each step, we incrementally check if a prefix of the string matches a corresponding suffix. When a match is found, it forms one palindromic chunk, and we recursively or iteratively process the remaining middle portion. This approach works because once a matching pair is identified, it is guaranteed that the optimal decomposition must include it to maximize k. Optionally, a rolling hash can optimize the string comparison, but for n <= 1000, direct substring comparison is sufficient.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(2^n) | O(n) | Recursively tries all split combinations; exponential time |
| Optimal | O(n^2) | O(1) | Two-pointer incremental comparison; substring checks dominate |
Algorithm Walkthrough
- Initialize two pointers,
leftstarting at the beginning of the string andrightat the end. Initialize a countercountto track the number of palindromic chunks. - While
left <= right, attempt to find the smallest non-empty prefix starting atleftthat matches a suffix ending atright. - Use an inner loop to incrementally expand the prefix size. At each step, check if
text[left:left+size] == text[right-size+1:right+1]. - If a match is found:
- Increment
countby 2 if the prefix and suffix are distinct, or by 1 ifleftandrightpoint to the same remaining substring (i.e., middle chunk in an odd-length decomposition). - Move
leftpast the matched prefix andrightbefore the matched suffix.
- Repeat until the pointers cross. Return
count.
Why it works: At each iteration, the algorithm greedily identifies the smallest matching prefix-suffix pair. Since any optimal solution must include the outermost matching chunks to maximize the total number of chunks, this approach guarantees the largest possible k. It avoids redundant work by only checking contiguous prefix-suffix pairs.
Python Solution
class Solution:
def longestDecomposition(self, text: str) -> int:
n = len(text)
left, right = 0, n - 1
count = 0
while left <= right:
size = 1
found = False
while left + size - 1 < right:
if text[left:left+size] == text[right-size+1:right+1]:
count += 2
left += size
right -= size
found = True
break
size += 1
if not found:
count += 1
break
return count
Implementation walkthrough: We initialize two pointers and a chunk counter. The inner loop checks prefixes against corresponding suffixes. When a match is found, we count the two chunks and move the pointers inward. If no match is found before pointers cross, the remaining middle substring is counted as one chunk.
Go Solution
func longestDecomposition(text string) int {
left, right := 0, len(text)-1
count := 0
for left <= right {
size := 1
found := false
for left+size-1 < right {
if text[left:left+size] == text[right-size+1:right+1] {
count += 2
left += size
right -= size
found = true
break
}
size++
}
if !found {
count++
break
}
}
return count
}
Go-specific notes: Go slices make substring comparison straightforward, similar to Python. The main difference is handling string slicing with [start:end] notation and explicitly managing indices.
Worked Examples
Example 1: "ghiabcdefhelloadamhelloabcdefghi"
| Step | left | right | size | Match? | count |
|---|---|---|---|---|---|
| 1 | 0 | 28 | 3 | Yes | 2 |
| 2 | 3 | 25 | 6 | Yes | 4 |
| 3 | 9 | 19 | 5 | Yes | 6 |
| 4 | 14 | 14 | 1 | Single remaining | 7 |
Example 2: "merchant"
No matching prefix-suffix until pointers cross. count = 1.
Example 3: "antaprezatepzapreanta"
| Step | left | right | size | Match? | count |
|---|---|---|---|---|---|
| 1 | 0 | 19 | 1 | Yes | 2 |
| 2 | 1 | 18 | 2 | Yes | 4 |
| 3 | 3 | 16 | 1 | Yes | 6 |
| 4 | 4 | 15 | 3 | Yes | 8 |
| 5 | 7 | 12 | 3 | Yes | 10 |
| 6 | 10 | 9 | Remaining middle | 11 |
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n^2) | For each outer iteration, we may scan up to n characters to find matching prefix-suffix pairs |
| Space | O(1) | Only pointers and counters are used, no additional data structures |
Even though substring comparison is linear in length, the total number of characters compared across all iterations is bounded by O(n^2). For n ≤ 1000, this is acceptable.
Test Cases
# Provided examples
assert Solution().longestDecomposition("ghiabcdefhelloadamhelloabcdefghi") == 7 # standard case
assert Solution().longestDecomposition("merchant") == 1 # no match
assert Solution().longestDecomposition("antaprezatepzapreanta") == 11 # multiple small chunks
# Boundary values
assert Solution().longestDecomposition("a") == 1 # single character
assert Solution().longestDecomposition("aa") == 2 # two identical characters
assert Solution().longestDecomposition("ab") == 1 # two distinct characters
# Stress case
assert Solution().longestDecomposition("abcde"*200) == 1 # long repeated pattern but no prefix-suffix match
| Test | Why |
|---|---|
| "ghiabcdefhelloadamhelloabcdefghi" | Standard decomposition with multiple chunks |
| "merchant" | No palindromic chunks except whole string |
| "antaprezatepzapreanta" | Odd-length decomposition with many small chunks |
| "a", "aa", "ab" | Boundary cases for short strings |
| Long repeated pattern | Tests efficiency on large input without matching chunks |
Edge Cases
Single-character string: For text = "a", the result must be 1. The algorithm correctly handles this because left equals right, and the loop counts the remaining middle chunk.
No matching prefix-suffix: For a string like "abcd", there are no palindromic chunks beyond the whole string. The inner loop fails to find a match, and the remaining string is counted as one chunk.
All identical characters: For text = "aaaa", the algorithm finds matching prefixes and suffixes at every step. It increments count correctly, producing a maximum decomposition of 4 chunks. This ensures the greedy approach works even when multiple valid prefix-suffix choices exist.