Backtracking Problems: Combination Sum and Palindrome Partitioning

39. Combination Sum

The key insight for this problem is understanding how elements can be reused during the search process. When recursively exploring combinations, each element can be selected multiple times since we continue searching from the current index rather than moving to the next one.

Consider the tree structure: after selecting an element, subsequent selections can include that element again or any element that follows it in the sorted array. This prevents duplicate combinations while allowing elements to be reused.

For problems involving a single collection to find combinations, we use startIndex to control which elements are available for selection. For example, LeetCode 77 (Combinations) and 216 (Combination Sum III) follow this pattern. However, when dealing with multiple independent collections where combinations from different sets don't affect eachother, there's no need for startIndex—such as in the Letter Combinations of a Phone Number problem.

class Solution:
    def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
        result = []
        path = []
        self.backtrack(candidates, 0, target, path, result)
        return result

    def backtrack(self, candidates: List[int], start: int, target: int, path: List[int], result: List[List[int]]) -> None:
        if sum(path) > target:
            return
        if sum(path) == target:
            result.append(path[:])
            return

        for i in range(start, len(candidates)):
            path.append(candidates[i])
            self.backtrack(candidates, i, target, path, result)
            path.pop()

Pruning Optimization

Sorting the candidates array first enables early termination during the search. When the running sum exceeds the target, we can break out of the loop since subsequent elements (in sorted order) will only increase the sum further.

class Solution:
    def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
        result = []
        path = []
        candidates.sort()
        self.backtrack(candidates, 0, target, path, result)
        return result

    def backtrack(self, candidates: List[int], start: int, target: int, path: List[int], result: List[List[int]]) -> None:
        if sum(path) == target:
            result.append(path[:])
            return

        for i in range(start, len(candidates)):
            if sum(path) > target:
                break
            path.append(candidates[i])
            self.backtrack(candidates, i, target, path, result)
            path.pop()

40. Combination Sum II

This problem differs from the previous one in that each element in the candidates array can only be used once. Additionally, the array may contain duplicate values, requiring careful handling to avoid generating identical combinations.

Handling Duplicates with Sorting

After sorting the candidates, we can skip duplicate elements during the backtracking process. The strategy is: when encountering duplicate values, only proceed with the first occurrence and skip all subsequent identical values at the same recursion level.

class Solution:
    def combinationSum2(self, candidates: List[int], target: int) -> List[List[int]]:
        result = []
        candidates.sort()
        self.backtrack(candidates, target, 0, 0, [], result)
        return result

    def backtrack(self, candidates: List[int], target: int, total: int, start: int, path: List[int], result: List[List[int]]) -> None:
        if total == target:
            result.append(path[:])
            return

        for i in range(start, len(candidates)):
            if i > start and candidates[i] == candidates[i - 1]:
                continue

            if total + candidates[i] > target:
                break

            total += candidates[i]
            path.append(candidates[i])
            self.backtrack(candidates, target, total, i + 1, path, result)
            total -= candidates[i]
            path.pop()

Handling Duplicates with Used Array

An alternative approach uses a used boolean array to track which elements have been selected in the current path. This approach provides more explicit control over element selection:

class Solution:
    def combinationSum2(self, candidates: List[int], target: int) -> List[List[int]]:
        result = []
        used = [False] * len(candidates)
        candidates.sort()
        self.backtrack(candidates, target, 0, 0, used, [], result)
        return result

    def backtrack(self, candidates: List[int], target: int, total: int, start: int, used: List[bool], path: List[int], result: List[List[int]]) -> None:
        if total == target:
            result.append(path[:])
            return

        for i in range(start, len(candidates)):
            if i > start and candidates[i] == candidates[i - 1] and not used[i - 1]:
                continue

            if total + candidates[i] > target:
                break

            total += candidates[i]
            path.append(candidates[i])
            used[i] = True
            self.backtrack(candidates, target, total, i + 1, used, path, result)
            used[i] = False
            total -= candidates[i]
            path.pop()

The condition i > start and candidates[i] == candidates[i - 1] and not used[i - 1] ensures that when we encounter a duplicate element, we only proceed with the first occurrence at each recursion level.

131. Palindrome Partitioning

This problem requires finding all ways to partition a string such that each substring is a palindrome. The approach uses backtracking where we extend the partition point incrementally and validate each substring.

The critical insight is that at each recursion level, we iterate from the current start position to the end of the string. For each position, we extract the substring and check if it's a palindrome. Only palindromic substrings are added to the current path before recursing with the next partition point.

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

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

        def backtrack(start: int) -> None:
            if start >= len(s):
                result.append(path[:])
                return

            for i in range(start, len(s)):
                substring = s[start:i + 1]
                if is_palindrome(substring):
                    path.append(substring)
                    backtrack(i + 1)
                    path.pop()

        backtrack(0)
        return result

The termination condition start >= len(s) triggers when we've processed the entire string, at which point a valid partition has been found. The loop range(start, len(s)) systematically explores all possible partition points, and the palindrome check s[start:i+1] == s[start:i+1][::-1] filters valid substrings using Python's string slicing with step -1.

Tags: algorithms backtracking combination palindrome python

Posted on Thu, 07 May 2026 06:45:14 +0000 by rachelk