131. Palindrome Partitioning - Explanation

Problem Link

Description

Given a string s, split s into substrings where every substring is a palindrome. Return all possible lists of palindromic substrings.

You may return the solution in any order.

Example 1:

Input: s = "aab"

Output: [["a","a","b"],["aa","b"]]

Example 2:

Input: s = "a"

Output: [["a"]]

Constraints:

  • 1 <= s.length <= 20
  • s contains only lowercase English letters.


Topics

Recommended Time & Space Complexity

You should aim for a solution with O(n * (2^n)) time and O(n) space, where n is the length of the input string.


Hint 1

For a given string there are 2^n possible partitions because at each index we have two decisions: we can either partition and start a new string, or continue without partitioning. Can you think of an algorithm to recursively traverse all combinations?


Hint 2

We can use backtracking to recursively traverse the string with indices j (start of the current substring) and i (current iterating index). At each step, we either skip partitioning at the current index or, if the substring from j to i is a palindrome, make a partition, update j = i + 1, and start a new substring. The base condition to stop the recursion is when j reaches the end of the string. How do you implement this?


Hint 3

We start with j = 0, i = 0 and a temporary list which stores the substrings from the partitions. Then we recursively iterate the string with the index i. At each step we apply the 2 decisions accordingly. At the base condition of the recursive path, we make a copy of the current partition list and add it to the result.


Company Tags

Please upgrade to NeetCode Pro to view company tags.



Prerequisites

Before attempting this problem, you should be comfortable with:

  • Backtracking - Exploring all possible partitions and undoing choices to try alternatives
  • Recursion - Breaking down the problem into smaller subproblems
  • Palindrome Checking - Using two pointers to verify if a substring is a palindrome
  • Dynamic Programming - Precomputing palindrome substrings for O(1) lookup

1. Backtracking - I

Intuition

We want to split the string into pieces, but we only keep a split if every piece is a palindrome.

Think of placing “cuts” in the string:

  • Start at some index j (start of the next piece).
  • Try to extend the end index i to form a substring s[j..i].
  • If s[j..i] is a palindrome, we choose it (put it into part) and then restart from the next position (i+1) to build the next piece.
  • Whether or not it was a palindrome, we can also extend further by moving i to i+1 (trying a longer substring from the same start j).

Backtracking means:

  • When we choose a palindrome piece, we go deeper.
  • After returning, we remove that piece and try other possibilities.

Algorithm

  1. Keep:
    • part: current list of chosen palindrome substrings.
    • res: all valid partitions.
  2. Use DFS with two pointers:
    • j = start index of the current substring we're trying to form.
    • i = end index we are expanding.
  3. If i reaches the end of the string:
    • If j is also at the end, it means we perfectly partitioned the whole string → add a copy of part to res.
    • Return.
  4. If substring s[j..i] is a palindrome:
    • Add it to part.
    • Recurse with next start/end: dfs(i+1, i+1).
    • Backtrack: remove the last added substring.
  5. Also try making the substring longer without cutting yet:
    • dfs(j, i+1).
  6. Palindrome check (isPali(l,r)): two pointers moving inward; if mismatch → return false.
class Solution:
    def partition(self, s: str) -> List[List[str]]:
        res, part = [], []

        def dfs(j, i):
            if i >= len(s):
                if i == j:
                    res.append(part.copy())
                return

            if self.isPali(s, j, i):
                part.append(s[j : i + 1])
                dfs(i + 1, i + 1)
                part.pop()

            dfs(j, i + 1)

        dfs(0, 0)
        return res

    def isPali(self, s, l, r):
        while l < r:
            if s[l] != s[r]:
                return False
            l, r = l + 1, r - 1
        return True

Time & Space Complexity

  • Time complexity: O(n2n)O(n * 2 ^ n)
  • Space complexity:
    • O(n)O(n) extra space.
    • O(n2n)O(n * 2 ^ n) space for the output list.

2. Backtracking - II

Intuition

We build the partition from left to right.

At any starting index i, we have a simple question:

“Where should I cut next?”

So we try every possible end index j from i to end:

  • If s[i..j] is a palindrome, it can be the next piece.
  • Choose it (add to part), then recursively solve the rest starting at j + 1.
  • After coming back, undo the choice (pop) and try a different j.

This guarantees:

  • We only add palindrome pieces.
  • We explore all valid ways to cut the string.

Algorithm

  1. Maintain:
    • part: current list of chosen substrings.
    • res: all palindrome partitions.
  2. Define DFS dfs(i) where i is the start index of the next substring.
  3. Base case:
    • If i == len(s), the whole string has been partitioned → add a copy of part to res.
  4. For each j from i to len(s)-1:
    • If s[i..j] is palindrome:
      • Add s[i..j] to part.
      • Recurse dfs(j + 1).
      • Backtrack: remove last substring.
  5. Palindrome check:
    • Two pointers l, r move inward; if mismatch → return false.
class Solution:

    def partition(self, s: str) -> List[List[str]]:
        res, part = [], []

        def dfs(i):
            if i >= len(s):
                res.append(part.copy())
                return
            for j in range(i, len(s)):
                if self.isPali(s, i, j):
                    part.append(s[i : j + 1])
                    dfs(j + 1)
                    part.pop()

        dfs(0)
        return res

    def isPali(self, s, l, r):
        while l < r:
            if s[l] != s[r]:
                return False
            l, r = l + 1, r - 1
        return True

Time & Space Complexity

  • Time complexity: O(n2n)O(n * 2 ^ n)
  • Space complexity:
    • O(n)O(n) extra space.
    • O(n2n)O(n * 2 ^ n) space for the output list.

3. Backtracking (DP)

Intuition

In plain backtracking, we repeatedly check whether substrings are palindromes, which costs extra time.
To optimize this, we precompute all palindrome substrings once using Dynamic Programming (DP).

Idea:

  • First, build a DP table dp[i][j] that tells whether s[i..j] is a palindrome.
  • Then use backtracking just like before, but instead of checking palindromes on the fly, we directly look up dp[i][j].

This makes backtracking faster because palindrome checks become O(1).

Algorithm

Step 1: Precompute Palindromes (DP)

  1. Create a 2D table dp where:
    • dp[i][j] = True if substring s[i..j] is a palindrome.
  2. Fill it by increasing substring length:
    • Single characters are palindromes.
    • For longer substrings:
      • s[i] == s[j] and inner substring is palindrome (or length ≤ 2).

Step 2: Backtracking

  1. Maintain:
    • part: current partition.
    • res: all valid palindrome partitions.
  2. Define dfs(i):
    • i = starting index for next substring.
  3. Base case:
    • If i == len(s), add a copy of part to res.
  4. Try all j from i to end:
    • If dp[i][j] is true:
      • Choose substring s[i..j].
      • Recurse on dfs(j + 1).
      • Backtrack (remove last choice).
class Solution:

    def partition(self, s: str) -> List[List[str]]:
        n = len(s)
        dp = [[False] * n for _ in range(n)]
        for l in range(1, n + 1):
            for i in range(n - l + 1):
                dp[i][i + l - 1] = (s[i] == s[i + l - 1] and
                                    (i + 1 > (i + l - 2) or
                                    dp[i + 1][i + l - 2]))

        res, part = [], []
        def dfs(i):
            if i >= len(s):
                res.append(part.copy())
                return
            for j in range(i, len(s)):
                if dp[i][j]:
                    part.append(s[i : j + 1])
                    dfs(j + 1)
                    part.pop()

        dfs(0)
        return res

Time & Space Complexity

  • Time complexity: O(n2n)O(n * 2 ^ n)
  • Space complexity:
    • O(n2)O(n ^ 2) extra space.
    • O(n2n)O(n * 2 ^ n) space for the output list.

4. Recursion

Intuition

This approach combines Dynamic Programming and pure recursion (return-based).

  • We first precompute all palindromic substrings using DP.
  • Then we use recursion where each recursive call:
    • returns all valid palindrome partitions starting from a given index.
  • Instead of maintaining a global result or path, each recursive call builds and returns its own list of partitions, which makes the logic clean and declarative.

Think of it as:

“All partitions starting at index i =
choose a palindrome s[i..j] + all partitions starting at j + 1

Algorithm

Step 1: Precompute Palindromes (DP)

  1. Create a 2D table dp[i][j].
  2. dp[i][j] = True if substring s[i..j] is a palindrome.
  3. Fill the table by increasing substring length:
    • Characters at ends must match.
    • Inner substring must already be a palindrome (or be empty).

Step 2: Recursive Construction

  1. Define dfs(i):
    • Returns all palindrome partitions starting from index i.
  2. Base case:
    • If i == len(s), return [[]] (one empty partition).
  3. Recursive case:
    • For every j from i to end:
      • If dp[i][j] is true:
        • Recursively get partitions from dfs(j + 1).
        • Prepend s[i..j] to each returned partition.
  4. Return the collected partitions.
class Solution:

    def partition(self, s: str) -> List[List[str]]:
        n = len(s)
        dp = [[False] * n for _ in range(n)]
        for l in range(1, n + 1):
            for i in range(n - l + 1):
                dp[i][i + l - 1] = (s[i] == s[i + l - 1] and
                                    (i + 1 > (i + l - 2) or
                                    dp[i + 1][i + l - 2]))

        def dfs(i):
            if i >= n:
                return [[]]

            ret = []
            for j in range(i, n):
                if dp[i][j]:
                    nxt = dfs(j + 1)
                    for part in nxt:
                        cur = [s[i : j + 1]] + part
                        ret.append(cur)
            return ret

        return dfs(0)

Time & Space Complexity

  • Time complexity: O(n2n)O(n * 2 ^ n)
  • Space complexity:
    • O(n2)O(n ^ 2) extra space.
    • O(n2n)O(n * 2 ^ n) space for the output list.

Common Pitfalls

Forgetting to Copy the Partition Before Adding to Results

When adding the current partition to the results list, you must add a copy of the list, not a reference to it. Since backtracking modifies the same partition list, adding the reference directly means all entries in the result will point to the same (eventually empty) list.

Incorrect Base Case in Backtracking

The base case should trigger when the starting index reaches the end of the string, indicating a complete valid partition. A common mistake is to check i > len(s) instead of i >= len(s) or i == len(s), causing missed partitions or index errors.

Redundant Palindrome Checks Without Memoization

Repeatedly checking whether the same substring is a palindrome across different recursion branches wastes time. Without precomputing palindrome information using DP, the same substring may be checked O(2^n) times, significantly slowing down the solution for longer strings.