1745. Palindrome Partitioning IV
Problem Description
Given a string s
, the task is to determine whether it's possible to split the string into exactly three non-empty palindromic substrings. Return true
if such a split is possible, otherwise return false
.
A palindrome is a string that reads the same forwards and backwards. For example, "aba", "racecar", and "a" are palindromes.
The key requirements are:
- The string must be split into exactly three parts
- Each part must be non-empty (contain at least one character)
- Each part must be a palindrome
- The three parts must be consecutive substrings that together form the original string
For example:
- If
s = "abcbdd"
, it can be split into"a"
,"bcb"
,"dd"
- all three are palindromes, so returntrue
- If
s = "bcbddxy"
, it cannot be split into three palindromic substrings, so returnfalse
The solution uses dynamic programming to pre-compute which substrings are palindromes, then checks all possible ways to partition the string into three parts to see if any valid partition exists.
Intuition
The naive approach would be to try all possible ways to split the string into three parts and check if each part is a palindrome. However, checking if a substring is a palindrome takes O(n) time, and with O(n²) possible splits, this would be inefficient.
The key insight is that we can precompute which substrings are palindromes to avoid redundant palindrome checks. Once we know which substrings are palindromes, we can efficiently check all possible three-way splits.
To build our palindrome information, we observe that a substring s[i..j]
is a palindrome if and only if:
- The first and last characters match (
s[i] == s[j]
), AND - Either it's a substring of length 1 or 2 (base cases), OR the inner substring
s[i+1..j-1]
is also a palindrome
This recursive relationship suggests using dynamic programming. We create a 2D table f[i][j]
where f[i][j] = true
if substring s[i..j]
is a palindrome.
Once we have this table, finding a valid three-way split becomes straightforward. We need to find two split points that divide the string into three parts. We can iterate through all possible positions for these two split points:
- First split after position
i
(creating substrings[0..i]
) - Second split after position
j
(creating substrings[i+1..j]
) - The third substring is automatically
s[j+1..n-1]
If all three parts are palindromes (which we can check in O(1) time using our precomputed table), we've found a valid partition. The beauty of this approach is that we reduce the problem from repeated substring checking to simple table lookups.
Learn more about Dynamic Programming patterns.
Solution Approach
The solution uses dynamic programming to efficiently determine all palindromic substrings, then checks all possible three-way partitions.
Step 1: Build the Palindrome Table
We create a 2D boolean table f[i][j]
where f[i][j] = True
if substring s[i..j]
is a palindrome. The table is initialized with all values as True
.
To fill this table, we use the state transition equation:
f[i][j] = (s[i] == s[j]) and (i + 1 == j or f[i + 1][j - 1])
This means substring s[i..j]
is a palindrome if:
- Characters at positions
i
andj
are equal, AND - Either the substring has length 2 (when
i + 1 == j
), OR - The inner substring
s[i+1..j-1]
is also a palindrome
Since f[i][j]
depends on f[i + 1][j - 1]
, we iterate i
from n-1
down to 0
(reverse order) and j
from i+1
to n-1
(forward order). This ensures that when computing f[i][j]
, the value f[i + 1][j - 1]
has already been computed.
Step 2: Check All Possible Three-Way Partitions
After building the palindrome table, we enumerate all possible ways to split the string into three parts:
- First substring:
s[0..i]
wherei
ranges from0
ton-3
- Second substring:
s[i+1..j]
wherej
ranges fromi+1
ton-2
- Third substring:
s[j+1..n-1]
(automatically determined by first two splits)
For each partition, we check if all three parts are palindromes:
f[0][i]
checks if first part is palindromef[i + 1][j]
checks if second part is palindromef[j + 1][n-1]
checks if third part is palindrome
If we find any partition where all three conditions are true, we immediately return True
.
Time Complexity: O(n²) for building the palindrome table and O(n²) for checking all partitions, giving overall O(n²).
Space Complexity: O(n²) for the palindrome table.
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 = "abcbdd"
.
Step 1: Build the Palindrome Table
We create a 6×6 table (string length is 6) to track which substrings are palindromes.
Starting with the table initialized to True
, we fill it by checking each substring:
For substring length 1 (single characters):
- All single characters are palindromes by default
For substring length 2:
s[0..1]
= "ab":s[0] ≠ s[1]
→f[0][1] = False
s[1..2]
= "bc":s[1] ≠ s[2]
→f[1][2] = False
s[2..3]
= "cb":s[2] ≠ s[3]
→f[2][3] = False
s[3..4]
= "bd":s[3] ≠ s[4]
→f[3][4] = False
s[4..5]
= "dd":s[4] = s[5]
→f[4][5] = True
✓
For substring length 3:
s[1..3]
= "bcb":s[1] = s[3] = 'b'
andf[2][2] = True
→f[1][3] = True
✓- Other length-3 substrings are not palindromes
Continuing this process, our final palindrome table shows:
- "a" (index 0-0): palindrome ✓
- "bcb" (index 1-3): palindrome ✓
- "dd" (index 4-5): palindrome ✓
- "abcbd" (index 0-4): not a palindrome
- And so on...
Step 2: Check All Three-Way Partitions
Now we try all possible ways to split into three parts:
Split at positions (0, 1):
- Part 1:
s[0..0]
= "a" →f[0][0] = True
✓ - Part 2:
s[1..1]
= "b" →f[1][1] = True
✓ - Part 3:
s[2..5]
= "cbdd" →f[2][5] = False
✗ - Not valid
Split at positions (0, 2):
- Part 1:
s[0..0]
= "a" →f[0][0] = True
✓ - Part 2:
s[1..2]
= "bc" →f[1][2] = False
✗ - Not valid
Split at positions (0, 3):
- Part 1:
s[0..0]
= "a" →f[0][0] = True
✓ - Part 2:
s[1..3]
= "bcb" →f[1][3] = True
✓ - Part 3:
s[4..5]
= "dd" →f[4][5] = True
✓ - Valid partition found! Return
True
The string "abcbdd" can be split into "a" + "bcb" + "dd", where all three parts are palindromes.
Solution Implementation
1class Solution:
2 def checkPartitioning(self, s: str) -> bool:
3 """
4 Check if a string can be partitioned into exactly 3 palindromic substrings.
5
6 Args:
7 s: Input string to be partitioned
8
9 Returns:
10 True if the string can be partitioned into 3 palindromes, False otherwise
11 """
12 n = len(s)
13
14 # Build a 2D DP table where is_palindrome[i][j] indicates
15 # whether substring s[i:j+1] is a palindrome
16 is_palindrome = [[True] * n for _ in range(n)]
17
18 # Fill the palindrome table using bottom-up approach
19 # Start from the end of string and work backwards
20 for start in range(n - 1, -1, -1):
21 for end in range(start + 1, n):
22 # A substring is palindrome if:
23 # 1. First and last characters match, AND
24 # 2. Either it's a 2-character string OR the inner substring is also palindrome
25 is_palindrome[start][end] = (
26 s[start] == s[end] and
27 (end - start == 1 or is_palindrome[start + 1][end - 1])
28 )
29
30 # Try all possible ways to split the string into 3 parts
31 # First cut after index i, second cut after index j
32 for first_cut in range(n - 2): # Leave room for at least 2 more characters
33 for second_cut in range(first_cut + 1, n - 1): # Leave room for last substring
34 # Check if all three parts are palindromes:
35 # Part 1: s[0:first_cut+1]
36 # Part 2: s[first_cut+1:second_cut+1]
37 # Part 3: s[second_cut+1:n]
38 if (is_palindrome[0][first_cut] and
39 is_palindrome[first_cut + 1][second_cut] and
40 is_palindrome[second_cut + 1][n - 1]):
41 return True
42
43 return False
44
1class Solution {
2 public boolean checkPartitioning(String s) {
3 int n = s.length();
4
5 // Create a 2D DP table to store palindrome information
6 // isPalindrome[i][j] represents whether substring from index i to j is a palindrome
7 boolean[][] isPalindrome = new boolean[n][n];
8
9 // Initialize all values to true (single characters and empty strings are palindromes)
10 for (boolean[] row : isPalindrome) {
11 Arrays.fill(row, true);
12 }
13
14 // Build the palindrome table using dynamic programming
15 // Start from the end of string and work backwards
16 for (int start = n - 1; start >= 0; --start) {
17 for (int end = start + 1; end < n; ++end) {
18 // A substring is a palindrome if:
19 // 1. First and last characters match
20 // 2. Either it's a 2-character string OR the inner substring is also a palindrome
21 isPalindrome[start][end] = s.charAt(start) == s.charAt(end) &&
22 (start + 1 == end || isPalindrome[start + 1][end - 1]);
23 }
24 }
25
26 // Try all possible ways to split the string into 3 parts
27 // First split at position i (creating substring [0, i])
28 for (int firstSplit = 0; firstSplit < n - 2; ++firstSplit) {
29 // Second split at position j (creating substring [i+1, j])
30 for (int secondSplit = firstSplit + 1; secondSplit < n - 1; ++secondSplit) {
31 // Check if all three parts are palindromes:
32 // Part 1: [0, firstSplit]
33 // Part 2: [firstSplit + 1, secondSplit]
34 // Part 3: [secondSplit + 1, n - 1]
35 if (isPalindrome[0][firstSplit] &&
36 isPalindrome[firstSplit + 1][secondSplit] &&
37 isPalindrome[secondSplit + 1][n - 1]) {
38 return true;
39 }
40 }
41 }
42
43 return false;
44 }
45}
46
1class Solution {
2public:
3 bool checkPartitioning(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 table using dynamic programming
10 // We iterate from bottom to top, right to left
11 for (int i = n - 1; i >= 0; --i) {
12 for (int j = i + 1; j < n; ++j) {
13 // A substring is palindrome if:
14 // 1. First and last characters match, AND
15 // 2. Either it's a 2-character string OR the inner substring is also palindrome
16 isPalindrome[i][j] = (s[i] == s[j]) && (j - i == 1 || isPalindrome[i + 1][j - 1]);
17 }
18 }
19
20 // Try all possible ways to partition string into 3 parts
21 // i represents the end of first part (inclusive)
22 // j represents the end of second part (inclusive)
23 for (int i = 0; i < n - 2; ++i) {
24 for (int j = i + 1; j < n - 1; ++j) {
25 // Check if all three parts are palindromes:
26 // Part 1: s[0...i]
27 // Part 2: s[i+1...j]
28 // Part 3: s[j+1...n-1]
29 if (isPalindrome[0][i] && isPalindrome[i + 1][j] && isPalindrome[j + 1][n - 1]) {
30 return true;
31 }
32 }
33 }
34
35 return false;
36 }
37};
38
1/**
2 * Checks if a string can be partitioned into exactly three palindromic substrings
3 * @param s - The input string to check
4 * @returns true if the string can be partitioned into three palindromes, false otherwise
5 */
6function checkPartitioning(s: string): boolean {
7 const length: number = s.length;
8
9 // Create a 2D DP array to store palindrome information
10 // isPalindrome[i][j] represents whether substring s[i...j] is a palindrome
11 const isPalindrome: boolean[][] = Array.from(
12 { length: length },
13 () => Array(length).fill(true)
14 );
15
16 // Build the palindrome lookup table using dynamic programming
17 // Start from the end of string and work backwards
18 for (let startIndex = length - 1; startIndex >= 0; startIndex--) {
19 for (let endIndex = startIndex + 1; endIndex < length; 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 // Try all possible ways to partition the string into three parts
30 // First partition: s[0...firstPartitionEnd]
31 // Second partition: s[firstPartitionEnd+1...secondPartitionEnd]
32 // Third partition: s[secondPartitionEnd+1...length-1]
33 for (let firstPartitionEnd = 0; firstPartitionEnd < length - 2; firstPartitionEnd++) {
34 for (let secondPartitionEnd = firstPartitionEnd + 1; secondPartitionEnd < length - 1; secondPartitionEnd++) {
35 // Check if all three partitions are palindromes
36 const isFirstPartPalindrome: boolean = isPalindrome[0][firstPartitionEnd];
37 const isSecondPartPalindrome: boolean = isPalindrome[firstPartitionEnd + 1][secondPartitionEnd];
38 const isThirdPartPalindrome: boolean = isPalindrome[secondPartitionEnd + 1][length - 1];
39
40 if (isFirstPartPalindrome && isSecondPartPalindrome && isThirdPartPalindrome) {
41 return true;
42 }
43 }
44 }
45
46 return false;
47}
48
Time and Space Complexity
The time complexity is O(n^2)
, where n
is the length of the string s
. This is because:
- The first nested loop (building the palindrome table
f
) iterates through all pairs(i, j)
wherei < j
, which takesO(n^2)
time - The second nested loop also iterates through all valid pairs
(i, j)
wherei
ranges from0
ton-3
andj
ranges fromi+1
ton-2
, which is alsoO(n^2)
in the worst case - Each operation inside both loops (array access and comparison) takes
O(1)
time - Therefore, the overall time complexity is
O(n^2) + O(n^2) = O(n^2)
The space complexity is O(n^2)
, where n
is the length of the string s
. This is because:
- The 2D array
f
is created with dimensionsn × n
, requiringO(n^2)
space to store boolean values indicating whether substringss[i:j+1]
are palindromes - No other significant auxiliary space is used besides a few integer variables for loop indices
- Therefore, the overall space complexity is
O(n^2)
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Palindrome Table Initialization
A common mistake is initializing the entire palindrome table as True
, which can lead to incorrect results for substrings that haven't been properly validated.
Pitfall Example:
# Incorrect - assumes all substrings are palindromes initially
is_palindrome = [[True] * n for _ in range(n)]
Solution:
Initialize only the diagonal (single characters) as True
, and set other values during computation:
is_palindrome = [[False] * n for _ in range(n)]
# Single characters are always palindromes
for i in range(n):
is_palindrome[i][i] = True
2. Wrong Iteration Order When Building Palindrome Table
The most critical pitfall is iterating in the wrong order when filling the palindrome table. Since is_palindrome[i][j]
depends on is_palindrome[i+1][j-1]
, we must ensure the inner substring is computed before the outer one.
Pitfall Example:
# Incorrect - forward iteration doesn't guarantee dependencies are met
for start in range(n):
for end in range(start + 1, n):
is_palindrome[start][end] = ... # May access uncomputed values
Solution: Use reverse iteration for the start index and forward iteration for the end index:
for start in range(n - 1, -1, -1): # Reverse order
for end in range(start + 1, n): # Forward order
is_palindrome[start][end] = (
s[start] == s[end] and
(end - start == 1 or is_palindrome[start + 1][end - 1])
)
3. Off-by-One Errors in Partition Boundaries
Confusing inclusive vs exclusive indices when checking partitions can lead to missing valid solutions or checking invalid partitions.
Pitfall Example:
# Incorrect bounds - might create empty partitions
for first_cut in range(n - 1): # Could go too far
for second_cut in range(first_cut, n): # Could create empty second part
Solution: Ensure each partition has at least one character:
for first_cut in range(n - 2): # Stop at n-2 to leave room for 2 more chars
for second_cut in range(first_cut + 1, n - 1): # Start after first_cut, stop before last
# This guarantees:
# - First part: [0, first_cut] has at least 1 char
# - Second part: [first_cut+1, second_cut] has at least 1 char
# - Third part: [second_cut+1, n-1] has at least 1 char
4. Handling Edge Cases for Length-2 Palindromes
Forgetting to handle the special case where a substring has exactly 2 characters can cause index out-of-bounds errors.
Pitfall Example:
# Incorrect - doesn't handle 2-character substrings properly is_palindrome[start][end] = ( s[start] == s[end] and is_palindrome[start + 1][end - 1] ) # When end - start == 1, this accesses is_palindrome[start+1][start]
Solution: Add explicit check for 2-character substrings:
is_palindrome[start][end] = ( s[start] == s[end] and (end - start == 1 or is_palindrome[start + 1][end - 1]) )
5. Memory Optimization Consideration
While not necessarily a pitfall, the O(n²) space complexity can be problematic for very long strings. Consider checking palindromes on-the-fly if memory is constrained, though this increases time complexity.
What's the output of running the following function using input [30, 20, 10, 100, 33, 12]
?
1def fun(arr: List[int]) -> List[int]:
2 import heapq
3 heapq.heapify(arr)
4 res = []
5 for i in range(3):
6 res.append(heapq.heappop(arr))
7 return res
8
1public static int[] fun(int[] arr) {
2 int[] res = new int[3];
3 PriorityQueue<Integer> heap = new PriorityQueue<>();
4 for (int i = 0; i < arr.length; i++) {
5 heap.add(arr[i]);
6 }
7 for (int i = 0; i < 3; i++) {
8 res[i] = heap.poll();
9 }
10 return res;
11}
12
1class HeapItem {
2 constructor(item, priority = item) {
3 this.item = item;
4 this.priority = priority;
5 }
6}
7
8class MinHeap {
9 constructor() {
10 this.heap = [];
11 }
12
13 push(node) {
14 // insert the new node at the end of the heap array
15 this.heap.push(node);
16 // find the correct position for the new node
17 this.bubble_up();
18 }
19
20 bubble_up() {
21 let index = this.heap.length - 1;
22
23 while (index > 0) {
24 const element = this.heap[index];
25 const parentIndex = Math.floor((index - 1) / 2);
26 const parent = this.heap[parentIndex];
27
28 if (parent.priority <= element.priority) break;
29 // if the parent is bigger than the child then swap the parent and child
30 this.heap[index] = parent;
31 this.heap[parentIndex] = element;
32 index = parentIndex;
33 }
34 }
35
36 pop() {
37 const min = this.heap[0];
38 this.heap[0] = this.heap[this.size() - 1];
39 this.heap.pop();
40 this.bubble_down();
41 return min;
42 }
43
44 bubble_down() {
45 let index = 0;
46 let min = index;
47 const n = this.heap.length;
48
49 while (index < n) {
50 const left = 2 * index + 1;
51 const right = left + 1;
52
53 if (left < n && this.heap[left].priority < this.heap[min].priority) {
54 min = left;
55 }
56 if (right < n && this.heap[right].priority < this.heap[min].priority) {
57 min = right;
58 }
59 if (min === index) break;
60 [this.heap[min], this.heap[index]] = [this.heap[index], this.heap[min]];
61 index = min;
62 }
63 }
64
65 peek() {
66 return this.heap[0];
67 }
68
69 size() {
70 return this.heap.length;
71 }
72}
73
74function fun(arr) {
75 const heap = new MinHeap();
76 for (const x of arr) {
77 heap.push(new HeapItem(x));
78 }
79 const res = [];
80 for (let i = 0; i < 3; i++) {
81 res.push(heap.pop().item);
82 }
83 return res;
84}
85
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
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
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!