Facebook Pixel

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 return true
  • If s = "bcbddxy", it cannot be split into three palindromic substrings, so return false

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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. The first and last characters match (s[i] == s[j]), AND
  2. 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 substring s[0..i])
  • Second split after position j (creating substring s[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 and j 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] where i ranges from 0 to n-3
  • Second substring: s[i+1..j] where j ranges from i+1 to n-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 palindrome
  • f[i + 1][j] checks if second part is palindrome
  • f[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 Evaluator

Example 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' and f[2][2] = Truef[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) where i < j, which takes O(n^2) time
  • The second nested loop also iterates through all valid pairs (i, j) where i ranges from 0 to n-3 and j ranges from i+1 to n-2, which is also O(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 dimensions n × n, requiring O(n^2) space to store boolean values indicating whether substrings s[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.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

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

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More