131. Palindrome Partitioning
Problem Description
Given a string s
, you need to partition it into substrings where every substring in the partition is a palindrome. A palindrome is a string that reads the same forwards and backwards. Your task is to return all possible ways to partition the string such that each part is a palindrome.
For example, if s = "aab"
, the valid partitions would be:
[["a","a","b"], ["aa","b"]]
The first partition breaks the string into three palindromes: "a", "a", and "b". The second partition breaks it into two palindromes: "aa" and "b".
The solution uses a two-step approach:
-
Preprocessing with Dynamic Programming: First, it builds a 2D table
f[i][j]
that stores whether the substring from indexi
toj
is a palindrome. This is done by checking if the characters at positionsi
andj
are equal and if the substring between them (fromi+1
toj-1
) is also a palindrome. The table is filled from bottom to top and left to right. -
Depth-First Search with Backtracking: The
dfs
function explores all possible partitions starting from indexi
. For each position, it tries all possible ending positionsj
where the substrings[i:j+1]
is a palindrome (checked using the preprocessed table). When a valid palindrome substring is found, it's added to the current partitiont
, and the search continues from positionj+1
. When the entire string is processed (i == n
), the current partition is added to the answer. The backtracking step removes the last added substring to explore other possibilities.
The algorithm systematically explores all valid ways to partition the string into palindromes and returns all possible partitioning schemes.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- No: The problem involves partitioning a string into palindromic substrings, not nodes and edges as in graph problems.
Need to solve for kth smallest/largest?
- No: The problem asks for all possible palindrome partitions, not finding the kth smallest or largest element.
Involves Linked Lists?
- No: The problem works with strings and their partitions, not linked list data structures.
Does the problem have small constraints?
- Yes: String partitioning problems typically have manageable constraints since we need to explore all possible ways to partition the string. The exponential nature of generating all partitions means the input size is usually limited.
Brute force / Backtracking?
- Yes: We need to explore all possible ways to partition the string where each partition is a palindrome. This requires trying different partition points, checking if substrings are palindromes, and backtracking when a path doesn't work out. The backtracking approach systematically explores all valid partitioning schemes.
Conclusion: The flowchart correctly leads us to use a Backtracking approach. This aligns perfectly with the solution, which uses DFS with backtracking to explore all possible palindrome partitions. The algorithm tries each valid palindrome substring, adds it to the current partition, recursively processes the remaining string, and backtracks to explore other possibilities.
Intuition
When we need to find all possible ways to partition a string into palindromes, we're essentially making a series of decisions: "Where should I make the next cut in the string?" At each position, we have multiple choices - we can cut after one character, two characters, three characters, and so on, as long as the resulting substring is a palindrome.
This decision-making process naturally forms a tree structure. Starting from the beginning of the string, we explore all valid palindrome substrings that start from index 0. For each valid palindrome we find, we've made a partition decision, and now we need to recursively solve the same problem for the remaining part of the string.
The key insight is that we need to explore ALL possible valid partitions, not just find one. This means we can't be greedy - we must systematically try every possibility. When we choose a palindrome substring and move forward, we might later need to come back and try a different partition point. This "try, explore, and undo" pattern is the essence of backtracking.
To make this efficient, we can precompute which substrings are palindromes. Without this preprocessing, we'd be checking the same substring multiple times during our exploration. By building a 2D table f[i][j]
that tells us whether substring from index i
to j
is a palindrome, we can instantly verify if a partition choice is valid during our backtracking process.
The backtracking works by maintaining a current partition list t
. We add a palindrome substring to this list, recursively explore the rest of the string, and then remove (backtrack) the substring to try other possibilities. When we've successfully partitioned the entire string (reached the end), we've found one valid solution and add it to our answer collection.
Solution Approach
The solution implements the backtracking approach with preprocessing optimization as described in the reference approach.
Step 1: Preprocessing Palindrome Information
First, we build a 2D boolean table f[i][j]
where f[i][j] = True
if the substring s[i..j]
is a palindrome. The table is filled using dynamic programming:
f = [[True] * n for _ in range(n)]
for i in range(n - 1, -1, -1):
for j in range(i + 1, n):
f[i][j] = s[i] == s[j] and f[i + 1][j - 1]
- We initialize all diagonal elements as
True
(single characters are palindromes) - We iterate from bottom to top (
i
fromn-1
to0
) and left to right (j
fromi+1
ton
) - A substring is a palindrome if its first and last characters match AND the substring between them is also a palindrome
- The condition
f[i][j] = s[i] == s[j] and f[i + 1][j - 1]
checks both conditions
Step 2: DFS with Backtracking
The dfs(i)
function explores all valid partitions starting from index i
:
def dfs(i: int):
if i == n:
ans.append(t[:])
return
for j in range(i, n):
if f[i][j]:
t.append(s[i : j + 1])
dfs(j + 1)
t.pop()
The algorithm works as follows:
-
Base Case: When
i == n
, we've successfully partitioned the entire string. We add a copy of the current partitiont
to our answer list. -
Recursive Case: For the current position
i
, we try all possible ending positionsj
fromi
ton-1
:- Check if
s[i..j]
is a palindrome using our preprocessed tablef[i][j]
- If it is, add this substring to our current partition
t
- Recursively process the remaining string starting from
j + 1
- Backtrack by removing the last added substring with
t.pop()
- Check if
Data Structures Used:
f
: 2D boolean array for palindrome lookup in O(1) timeans
: List to store all valid partition combinationst
: Temporary list to build current partition during DFS
The time complexity is O(n × 2^n) where n is the length of the string, as in the worst case we might have 2^n possible partitions and each takes O(n) to build. The space complexity is O(n^2) for the palindrome table plus O(n) for the recursion stack depth.
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 solution with s = "aab"
:
Step 1: Build the Palindrome Table
We create a 3×3 table f
where f[i][j]
indicates if substring from index i
to j
is a palindrome:
Initial table (all diagonals are True): 0 1 2 0 [ T ? ? ] 1 [ - T ? ] 2 [ - - T ]
Now we fill the table from bottom-right to top-left:
f[1][2]
: Is "ab" a palindrome?s[1] == s[2]
→ 'a' ≠ 'b' → Falsef[0][1]
: Is "aa" a palindrome?s[0] == s[1]
→ 'a' == 'a' → Truef[0][2]
: Is "aab" a palindrome?s[0] == s[2]
→ 'a' ≠ 'b' → False
Final table: 0 1 2 0 [ T T F ] 1 [ - T F ] 2 [ - - T ]
Step 2: DFS with Backtracking
Starting with dfs(0)
, t = []
, ans = []
:
-
dfs(0): Try partitions starting at index 0
j = 0
:f[0][0] = True
(substring "a")- Add "a" to
t
:t = ["a"]
- Call
dfs(1)
- Add "a" to
-
dfs(1): Try partitions starting at index 1
j = 1
:f[1][1] = True
(substring "a")- Add "a" to
t
:t = ["a", "a"]
- Call
dfs(2)
- Add "a" to
-
dfs(2): Try partitions starting at index 2
j = 2
:f[2][2] = True
(substring "b")- Add "b" to
t
:t = ["a", "a", "b"]
- Call
dfs(3)
- Add "b" to
-
dfs(3): Base case
i == 3
- Add
["a", "a", "b"]
toans
- Return
- Add
-
Back to dfs(2): Pop "b", no more j values, return
-
Back to dfs(1): Pop "a"
j = 2
:f[1][2] = False
(substring "ab" not palindrome)- No more j values, return
-
Back to dfs(0): Pop "a"
j = 1
:f[0][1] = True
(substring "aa")- Add "aa" to
t
:t = ["aa"]
- Call
dfs(2)
- Add "aa" to
-
dfs(2): Try partitions starting at index 2
j = 2
:f[2][2] = True
(substring "b")- Add "b" to
t
:t = ["aa", "b"]
- Call
dfs(3)
- Add "b" to
-
dfs(3): Base case
- Add
["aa", "b"]
toans
- Return
- Add
-
Backtrack and complete remaining iterations
Final Result: ans = [["a", "a", "b"], ["aa", "b"]]
The algorithm systematically explores all valid palindrome partitions through DFS, using the precomputed table for O(1) palindrome checks, and backtracks to try alternative partition points.
Solution Implementation
1class Solution:
2 def partition(self, s: str) -> List[List[str]]:
3 def backtrack(start_index: int) -> None:
4 """
5 Recursively find all palindrome partitions starting from start_index.
6
7 Args:
8 start_index: Current position in the string to start partitioning from
9 """
10 # Base case: reached the end of string, add current partition to results
11 if start_index == string_length:
12 result.append(current_partition[:])
13 return
14
15 # Try all possible end positions for the current substring
16 for end_index in range(start_index, string_length):
17 # If substring s[start_index:end_index+1] is a palindrome
18 if is_palindrome[start_index][end_index]:
19 # Add this palindrome substring to current partition
20 current_partition.append(s[start_index : end_index + 1])
21 # Recursively partition the remaining string
22 backtrack(end_index + 1)
23 # Backtrack: remove the last added substring
24 current_partition.pop()
25
26 string_length = len(s)
27
28 # Build DP table for palindrome check
29 # is_palindrome[i][j] = True if s[i:j+1] is a palindrome
30 is_palindrome = [[True] * string_length for _ in range(string_length)]
31
32 # Fill the DP table bottom-up
33 # Process from right to left for starting position
34 for i in range(string_length - 1, -1, -1):
35 # Process from left to right for ending position (j > i)
36 for j in range(i + 1, string_length):
37 # A substring is palindrome if:
38 # 1. First and last characters match
39 # 2. Inner substring is also a palindrome (or length <= 2)
40 is_palindrome[i][j] = s[i] == s[j] and is_palindrome[i + 1][j - 1]
41
42 # Initialize result list and current partition tracker
43 result = []
44 current_partition = []
45
46 # Start backtracking from index 0
47 backtrack(0)
48
49 return result
50
1class Solution {
2 private int stringLength;
3 private String inputString;
4 private boolean[][] isPalindrome;
5 private List<String> currentPartition = new ArrayList<>();
6 private List<List<String>> allPartitions = new ArrayList<>();
7
8 public List<List<String>> partition(String s) {
9 // Initialize variables
10 stringLength = s.length();
11 inputString = s;
12
13 // Create a 2D array to store palindrome information
14 // isPalindrome[i][j] = true if substring from index i to j is a palindrome
15 isPalindrome = new boolean[stringLength][stringLength];
16
17 // Initialize all single characters as palindromes
18 for (int i = 0; i < stringLength; ++i) {
19 Arrays.fill(isPalindrome[i], true);
20 }
21
22 // Build palindrome lookup table using dynamic programming
23 // Start from the end of string and work backwards
24 for (int startIndex = stringLength - 1; startIndex >= 0; --startIndex) {
25 for (int endIndex = startIndex + 1; endIndex < stringLength; ++endIndex) {
26 // A substring is a palindrome if:
27 // 1. First and last characters match
28 // 2. The substring between them is also a palindrome
29 isPalindrome[startIndex][endIndex] =
30 s.charAt(startIndex) == s.charAt(endIndex) &&
31 isPalindrome[startIndex + 1][endIndex - 1];
32 }
33 }
34
35 // Start DFS to find all palindrome partitions
36 findPartitions(0);
37 return allPartitions;
38 }
39
40 private void findPartitions(int startIndex) {
41 // Base case: if we've processed the entire string, add current partition to result
42 if (startIndex == inputString.length()) {
43 allPartitions.add(new ArrayList<>(currentPartition));
44 return;
45 }
46
47 // Try all possible end positions for the current substring
48 for (int endIndex = startIndex; endIndex < stringLength; ++endIndex) {
49 // If substring from startIndex to endIndex is a palindrome
50 if (isPalindrome[startIndex][endIndex]) {
51 // Add this palindrome substring to current partition
52 currentPartition.add(inputString.substring(startIndex, endIndex + 1));
53
54 // Recursively partition the remaining string
55 findPartitions(endIndex + 1);
56
57 // Backtrack: remove the last added substring
58 currentPartition.remove(currentPartition.size() - 1);
59 }
60 }
61 }
62}
63
1class Solution {
2public:
3 vector<vector<string>> partition(string s) {
4 int n = s.size();
5
6 // dp[i][j] represents whether substring s[i...j] is a palindrome
7 vector<vector<bool>> isPalindrome(n, vector<bool>(n, true));
8
9 // Build palindrome lookup table using dynamic programming
10 // For any substring, it's a palindrome if:
11 // 1. First and last characters match AND
12 // 2. The substring between them is also a palindrome
13 for (int i = n - 1; i >= 0; --i) {
14 for (int j = i + 1; j < n; ++j) {
15 isPalindrome[i][j] = (s[i] == s[j]) && isPalindrome[i + 1][j - 1];
16 }
17 }
18
19 // Store all valid palindrome partitions
20 vector<vector<string>> result;
21 // Current partition being built
22 vector<string> currentPartition;
23
24 // Recursive function to find all palindrome partitions
25 function<void(int)> backtrack = [&](int startIndex) -> void {
26 // Base case: reached end of string, add current partition to result
27 if (startIndex == n) {
28 result.push_back(currentPartition);
29 return;
30 }
31
32 // Try all possible end positions for current substring
33 for (int endIndex = startIndex; endIndex < n; ++endIndex) {
34 // If substring s[startIndex...endIndex] is palindrome
35 if (isPalindrome[startIndex][endIndex]) {
36 // Add this palindrome substring to current partition
37 currentPartition.push_back(s.substr(startIndex, endIndex - startIndex + 1));
38 // Recursively partition the remaining string
39 backtrack(endIndex + 1);
40 // Backtrack: remove the last added substring
41 currentPartition.pop_back();
42 }
43 }
44 };
45
46 // Start partitioning from index 0
47 backtrack(0);
48
49 return result;
50 }
51};
52
1/**
2 * Partitions a string into all possible palindrome substrings
3 * @param s - The input string to partition
4 * @returns A list of all possible palindrome partitioning of the string
5 */
6function partition(s: string): string[][] {
7 const stringLength: number = s.length;
8
9 // Dynamic programming table to store palindrome status
10 // isPalindrome[i][j] indicates whether substring from index i to j is a palindrome
11 const isPalindrome: boolean[][] = Array.from(
12 { length: stringLength },
13 () => Array(stringLength).fill(true)
14 );
15
16 // Build the palindrome lookup table using dynamic programming
17 // Start from the end and work backwards to ensure subproblems are solved first
18 for (let startIndex = stringLength - 1; startIndex >= 0; startIndex--) {
19 for (let endIndex = startIndex + 1; endIndex < stringLength; endIndex++) {
20 // A substring is a palindrome if:
21 // 1. First and last characters match
22 // 2. The substring between them is also a palindrome (or empty)
23 isPalindrome[startIndex][endIndex] =
24 s[startIndex] === s[endIndex] &&
25 isPalindrome[startIndex + 1][endIndex - 1];
26 }
27 }
28
29 // Store all valid palindrome partitions
30 const result: string[][] = [];
31
32 // Current partition being built
33 const currentPartition: string[] = [];
34
35 /**
36 * Depth-first search to find all palindrome partitions
37 * @param currentIndex - The current starting index in the string
38 */
39 const findPartitions = (currentIndex: number): void => {
40 // Base case: reached the end of the string
41 if (currentIndex === stringLength) {
42 // Add a copy of the current valid partition to results
43 result.push(currentPartition.slice());
44 return;
45 }
46
47 // Try all possible end positions for the current substring
48 for (let endIndex = currentIndex; endIndex < stringLength; endIndex++) {
49 // Check if substring from currentIndex to endIndex is a palindrome
50 if (isPalindrome[currentIndex][endIndex]) {
51 // Add this palindrome substring to current partition
52 currentPartition.push(s.slice(currentIndex, endIndex + 1));
53
54 // Recursively partition the remaining string
55 findPartitions(endIndex + 1);
56
57 // Backtrack: remove the last added substring
58 currentPartition.pop();
59 }
60 }
61 };
62
63 // Start the DFS from index 0
64 findPartitions(0);
65
66 return result;
67}
68
Time and Space Complexity
Time Complexity: O(n × 2^n)
The algorithm uses backtracking to generate all possible palindrome partitions. The preprocessing step to build the palindrome table f
takes O(n^2)
time. For the backtracking part, in the worst case where every substring is a palindrome (like "aaaa"), we need to decide at each position whether to make a cut or not, leading to 2^(n-1)
possible partitions. For each valid partition, we need O(n)
time to construct the substring and add it to the result. Therefore, the overall time complexity is O(n × 2^n)
.
Space Complexity: O(n^2)
The space complexity consists of several components:
- The 2D table
f
for storing palindrome information requiresO(n^2)
space - The recursion stack depth can go up to
O(n)
in the worst case - The temporary list
t
stores at mostn
elements, requiringO(n)
space - The result list
ans
stores all partitions, but this is not counted as auxiliary space since it's part of the output
The dominant factor is the palindrome table f
, making the overall space complexity O(n^2)
.
Common Pitfalls
1. Incorrect DP Table Initialization for Edge Cases
A common mistake is not properly handling the DP table initialization, particularly for substrings of length 1 and 2. While the provided solution initializes the entire table to True
, which works because:
- Diagonal elements (single characters) should be
True
- We only fill positions where
j > i
, so the initialTrue
values forj <= i
don't affect the result
However, developers often make errors when trying to optimize or modify this initialization:
Incorrect approach:
# Wrong: Forgetting that length-2 palindromes need special handling
is_palindrome = [[False] * n for _ in range(n)]
for i in range(n):
is_palindrome[i][i] = True # Only handling length-1
# This fails for adjacent equal characters like "aa"
Correct approach:
# The current solution handles this elegantly by: # 1. Initializing everything to True # 2. Only checking i+1 to j-1 when j > i, which naturally handles length-2 cases
2. Index Out of Bounds When Checking Inner Substring
Another pitfall occurs when checking is_palindrome[i + 1][j - 1]
without ensuring it's valid:
Problem scenario:
When j = i + 1
(substring of length 2), accessing is_palindrome[i + 1][j - 1]
means accessing is_palindrome[i + 1][i]
, which is checking a "reverse" range.
Why the current solution works:
The initialization of all values to True
means is_palindrome[i + 1][i]
is True
, which is semantically correct (an empty substring between two matching characters forms a palindrome).
Alternative explicit solution:
for i in range(n - 1, -1, -1):
for j in range(i + 1, n):
if j - i == 1: # Length 2 substring
is_palindrome[i][j] = s[i] == s[j]
else: # Length > 2
is_palindrome[i][j] = s[i] == s[j] and is_palindrome[i + 1][j - 1]
3. Creating Deep vs Shallow Copies
Common mistake:
# Wrong: This creates references, not copies result.append(current_partition) # All results will point to the same list!
Correct approach (as in the solution):
result.append(current_partition[:]) # Creates a new list copy
Without creating a copy, all entries in result
would reference the same list object, which gets modified during backtracking, resulting in an empty or incorrect final result.
4. Incorrect Loop Direction in DP Table Construction
Wrong approach:
# Iterating in wrong direction
for i in range(n):
for j in range(i + 1, n):
is_palindrome[i][j] = s[i] == s[j] and is_palindrome[i + 1][j - 1]
This fails because when we check is_palindrome[i + 1][j - 1]
, that value hasn't been computed yet (we're going top-to-bottom, but need values from below).
Correct approach: Iterate from bottom to top (i
from n-1
to 0
) to ensure required values are computed before being used.
How does quick sort divide the problem into subproblems?
Recommended Readings
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
Backtracking Template Prereq DFS with States problems dfs_with_states Combinatorial search problems Combinatorial search problems involve finding groupings and assignments of objects that satisfy certain conditions Finding all permutations combinations subsets and solving Sudoku are classic combinatorial problems The time complexity of combinatorial problems often grows rapidly with the size of
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Don’t Miss This!