-
Notifications
You must be signed in to change notification settings - Fork 273
Palindrome Partitioning
TIP103 Unit 8 Session 2 (Click for link to problem statements)
- 💡 Difficulty: Medium
- ⏰ Time to complete: 25-35 mins
- 🛠️ Topics: Backtracking, Recursion, Strings, Palindromes
Understand what the interviewer is asking for by using test cases and questions about the problem.
- Established a set (2-3) of test cases to verify their own solution later.
- Established a set (1-2) of edge cases to verify their solution handles complexities.
- Have fully understood the problem and have no clarifying questions.
- Have you verified any Time/Space Constraints for this problem?
-
What counts as a valid partition?
- A split of
sinto consecutive, non-overlapping substrings that concatenate back tos, where every substring is a palindrome.
- A split of
-
Do we return one partition or all of them?
- All possible palindrome partitionings, as a list of lists of strings, in any order.
-
Is a single character a palindrome?
- Yes. Every one-character string reads the same forwards and backwards, so splitting
sinto individual characters is always a valid partition.
- Yes. Every one-character string reads the same forwards and backwards, so splitting
HAPPY CASE
Input: s = "aab"
Output: [['a', 'a', 'b'], ['aa', 'b']]
Explanation: "aab" can be split as 'a' + 'a' + 'b' (all single characters are palindromes) or as 'aa' + 'b' ('aa' is a palindrome). 'aab' itself and 'ab' are not palindromes, so no other partition works.
EDGE CASE
Input: s = "a"
Output: [['a']]
Explanation: A single character has exactly one partition: itself.
Input: s = "ab"
Output: [['a', 'b']]
Explanation: 'ab' is not a palindrome, so the only valid partition splits the string into single characters.
Match what this problem looks like to known categories of problems, e.g. Linked List or Dynamic Programming, and strategies or patterns in those categories.
For Generate All Combinations/Partitions problems, we can consider the following approaches:
- Backtracking (DFS): At each position, try every prefix that is a palindrome, recurse on the rest of the string, and undo the choice before trying the next prefix. This is the standard pattern whenever a problem asks for all valid answers rather than a count or a best answer.
-
Dynamic Programming (optimization): A DP table of "is
s[i:j]a palindrome?" can be precomputed to avoid re-checking substrings, but the enumeration itself still requires backtracking.
Plan the solution with appropriate visualizations and pseudocode.
General Idea:
Build partitions one piece at a time. From a starting index, cut off every possible prefix; if the prefix is a palindrome, add it to the current partition and recurse on the remainder of the string. When the starting index reaches the end of the string, the current partition is complete — record a copy of it. After each recursive call, pop the piece off (backtrack) so the next prefix length can be tried.
1) Initialize an empty `result` list and an empty `current` partition.
2) Define a helper `backtrack(start)`:
a) Base case: if `start` equals the length of `s`, append a copy of `current` to `result` and return.
b) For each `end` from `start + 1` to `len(s)`:
i) Let `piece = s[start:end]`.
ii) If `piece` is a palindrome (reads the same reversed):
- Append `piece` to `current`.
- Recurse with `backtrack(end)`.
- Pop `piece` off `current` to undo the choice.
3) Call `backtrack(0)` and return `result`.
- Appending
currentitself instead of a copy (current[:]), so every recorded partition mutates into the same empty list by the end. - Off-by-one on the slice: forgetting that
range(start + 1, len(s) + 1)must reachlen(s)so the final character can end a piece. - Forgetting to pop after the recursive call, which corrupts the partition for later prefix choices.
- Recursing on non-palindrome prefixes and trying to filter at the end, which explodes the search space instead of pruning early.
Implement the code to solve the algorithm.
def partition(s):
def is_palindrome(sub):
return sub == sub[::-1]
def backtrack(start, current):
# Base case: every character is placed in a palindrome piece
if start == len(s):
result.append(current[:]) # record a copy of the finished partition
return
# Try every possible end index for the next piece
for end in range(start + 1, len(s) + 1):
piece = s[start:end]
if is_palindrome(piece):
current.append(piece) # choose
backtrack(end, current) # explore the rest of the string
current.pop() # un-choose (backtrack)
result = []
backtrack(0, [])
return resultReview the code by running specific example(s) and recording values (watchlist) of your code's variables along the way.
-
Input: s = "aab"
-
backtrack(0): piece'a'(palindrome) → recurse.-
backtrack(1): piece'a'(palindrome) → recurse.-
backtrack(2): piece'b'(palindrome) → recurse.-
backtrack(3): start == len(s), record['a', 'a', 'b'].
-
- Piece
'ab'is not a palindrome — skip.
-
- Piece
'aa'from index 0 (palindrome) → recurse.-
backtrack(2): piece'b'(palindrome) → recurse → record['aa', 'b'].
-
-
- Piece
'aab'is not a palindrome — skip. - Output: 'a', 'a', 'b'], ['aa', 'b'
-
-
Input: s = "a"
-
backtrack(0)chooses'a', thenbacktrack(1)records['a']. - Output: 'a'
-
Evaluate the performance of your algorithm and state any strong/weak or future potential work.
Assume N is the length of the string s.
-
Time Complexity:
O(N * 2^N)because there are up to2^(N-1)ways to place cuts in the string, and for each partition explored we doO(N)work checking palindromes and copying the finished partition. -
Space Complexity:
O(N)auxiliary space for the recursion stack and thecurrentpartition (excluding the output list, which itself can holdO(N * 2^N)characters in the worst case, e.g.s = "aaaa...").