1147. Longest Chunked Palindrome Decomposition
Problem Description
You are given a string text
that needs to be split into k substrings (subtext₁, subtext₂, ..., textₖ)
with the following conditions:
- Each substring must be non-empty
- When concatenated together, all substrings must equal the original
text
(i.e.,subtext₁ + subtext₂ + ... + subtextₖ == text
) - The substrings must form a palindromic pattern where
subtextᵢ == subtextₖ₋ᵢ₊₁
for all valid values of i (where1 ≤ 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.
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:
- Start with pointers at the beginning and end of the string
- Try to find the shortest substring from the left that matches a substring from the right
- When found, count it as 2 pieces and move both pointers inward
- Repeat until the pointers meet or cross
- 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 (indexlen(text) - 1
)
The algorithm works as follows:
-
Initialize variables:
ans = 0
to count the number of chunksi = 0
andj = len(text) - 1
as our two pointers
-
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
- Set
-
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 byk
positions:i += k
- Move
j
backward byk
positions:j -= k
- Set
ok = True
and break from the inner loop
- If no match, increment
k
to try a longer substring
- Check if
- While
-
Handle the middle portion:
- If
ok
is stillFalse
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
- If
-
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 EvaluatorExample 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 iftext[0:1]
("a") equalstext[4:5]
("b")- "a" ≠ "b", no match
- Try
k = 2
: Check iftext[0:2]
("ab") equalstext[3:5]
("ab")- "ab" = "ab", match found!
- Add 2 to answer:
ans = 2
- Move pointers:
i = 2
,j = 2
Iteration 2:
- Now
i = 2
andj = 2
(both pointing to the middle 'c') - Since
i <= j
is still true, we continue - The remaining substring is just "c"
- Try
k = 1
: Buti + k - 1 = 2
is not less thanj - 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 ton/2
times wheren
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 ton/2
(when no matching prefix-suffix pairs are found until the middle) - String slicing
text[i : i + k]
andtext[j - k + 1 : j + 1]
takesO(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.
Which of the following problems can be solved with backtracking (select multiple)
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!