Want Help Cracking FAANG?

(Then click this)

×
Back to Question Bank

131. Palindrome Partitioning - Leetcode Solution

Problem Description

The Palindrome Partitioning problem asks you to partition a given string s into all possible lists of substrings such that every substring in each list is a palindrome. A palindrome is a string that reads the same forwards and backwards (e.g., "aba", "aa", "a").

You must return all possible palindrome partitionings of s. Each partitioning should be a list of non-empty substrings, and each substring in the list should be a palindrome.

  • Each character in s can be used only once in each partitioning (no overlaps or reuse).
  • The order of characters in s must be preserved in each partition.
  • Return all valid partitions, not just one.

Example:
Input: s = "aab"
Output: [["a","a","b"], ["aa","b"]]

Thought Process

The first step is to recognize that the problem is about exploring all possible ways to break the string into substrings, and for each way, checking if all substrings are palindromes.

A brute-force approach would involve generating every possible way to partition the string, and then checking each substring for the palindrome property. However, this is inefficient because many partitions will contain non-palindromic substrings.

To optimize, we can use backtracking. As we build each partition, we only proceed with substrings that are palindromes. This way, we prune unnecessary branches early and avoid checking invalid partitions.

Think of the problem as exploring a tree: at each character, you can choose to "cut" and start a new substring, but only if the substring formed so far is a palindrome.

Solution Approach

  • Backtracking: We use a recursive function to explore all possible partitions. At each recursion, we try every possible end index for the current substring, and if the substring s[start:end+1] is a palindrome, we recursively partition the rest of the string from end+1.
  • Palindrome Check: For each substring considered, we need to check if it is a palindrome. This can be done by comparing the substring to its reverse or by comparing characters from both ends moving towards the center.
  • Path Tracking: We maintain a path (a list of substrings) that represents the current partition. When we reach the end of the string, we add a copy of the path to the result.
  • Backtrack: After exploring a partition, we backtrack by removing the last substring and trying the next possibility.
  • Optional Optimization: To avoid redundant palindrome checks, we can precompute a table (dynamic programming) that tells whether s[i:j+1] is a palindrome. However, for most input sizes, the straightforward backtracking is sufficient and easy to implement.

The process continues until all possible partitions have been explored.

Example Walkthrough

Let's walk through the input s = "aab" step by step:

  1. Start at index 0. Try substrings:
    • "a" (palindrome). Path = ["a"]. Recurse from index 1.
    • From index 1, try:
      • "a" (palindrome). Path = ["a", "a"]. Recurse from index 2.
      • From index 2, try:
        • "b" (palindrome). Path = ["a", "a", "b"]. Recurse from index 3 (end of string). Add ["a", "a", "b"] to result.
      • Backtrack. Try "ab" (not a palindrome). Skip.
    • Backtrack to index 0. Try "aa" (palindrome). Path = ["aa"]. Recurse from index 2.
    • From index 2, try:
      • "b" (palindrome). Path = ["aa", "b"]. Recurse from index 3 (end of string). Add ["aa", "b"] to result.
    • Backtrack. Try "aab" (not a palindrome). Skip.

The final result is [["a","a","b"], ["aa","b"]].

Time and Space Complexity

  • Brute-force approach: The number of possible partitions of a string of length n is 2^{n-1} (since each position between characters can be a cut or not). For each partition, we may need to check each substring for palindrome-ness, which can take O(n) time per substring. So the brute-force time complexity is roughly O(n \cdot 2^n).
  • Optimized backtracking: We only proceed with palindromic substrings, pruning invalid partitions early. However, in the worst case (all characters are the same), the number of partitions is still exponential: O(2^n). Each recursive call adds/removes from the current path, and each valid partition is copied into the result.
  • Space complexity: The recursion stack can go up to O(n) depth, and the result list can store up to O(2^n) partitionings, each containing up to n substrings. So overall space is O(n \cdot 2^n).

Summary

The Palindrome Partitioning problem is a classic example of using backtracking to explore all possible solutions efficiently. By only continuing with palindromic substrings, we avoid unnecessary work and prune invalid paths early. The approach is elegant because it leverages recursion, path tracking, and the palindrome property to systematically build all valid partitions. While the worst-case time is exponential, the solution is clean, intuitive, and works well for reasonable input sizes.

Code Implementation

class Solution:
    def partition(self, s: str):
        result = []
        path = []

        def is_palindrome(sub):
            return sub == sub[::-1]

        def backtrack(start):
            if start == len(s):
                result.append(path[:])
                return
            for end in range(start, len(s)):
                if is_palindrome(s[start:end+1]):
                    path.append(s[start:end+1])
                    backtrack(end+1)
                    path.pop()
        backtrack(0)
        return result
      
class Solution {
public:
    vector<vector<string>> partition(string s) {
        vector<vector<string>> result;
        vector<string> path;

        function<void(int)> backtrack = [&](int start) {
            if (start == s.size()) {
                result.push_back(path);
                return;
            }
            for (int end = start; end < s.size(); ++end) {
                if (isPalindrome(s, start, end)) {
                    path.push_back(s.substr(start, end - start + 1));
                    backtrack(end + 1);
                    path.pop_back();
                }
            }
        };

        backtrack(0);
        return result;
    }
private:
    bool isPalindrome(const string& s, int left, int right) {
        while (left < right) {
            if (s[left++] != s[right--]) return false;
        }
        return true;
    }
};
      
import java.util.*;

class Solution {
    public List<List<String>> partition(String s) {
        List<List<String>> result = new ArrayList<>();
        List<String> path = new ArrayList<>();
        backtrack(s, 0, path, result);
        return result;
    }

    private void backtrack(String s, int start, List<String> path, List<List<String>> result) {
        if (start == s.length()) {
            result.add(new ArrayList<>(path));
            return;
        }
        for (int end = start; end < s.length(); end++) {
            if (isPalindrome(s, start, end)) {
                path.add(s.substring(start, end + 1));
                backtrack(s, end + 1, path, result);
                path.remove(path.size() - 1);
            }
        }
    }

    private boolean isPalindrome(String s, int left, int right) {
        while (left < right) {
            if (s.charAt(left++) != s.charAt(right--)) return false;
        }
        return true;
    }
}
      
var partition = function(s) {
    const result = [];
    const path = [];

    function isPalindrome(str, left, right) {
        while (left < right) {
            if (str[left] !== str[right]) return false;
            left++;
            right--;
        }
        return true;
    }

    function backtrack(start) {
        if (start === s.length) {
            result.push([...path]);
            return;
        }
        for (let end = start; end < s.length; end++) {
            if (isPalindrome(s, start, end)) {
                path.push(s.substring(start, end + 1));
                backtrack(end + 1);
                path.pop();
            }
        }
    }

    backtrack(0);
    return result;
};