Backtracking

Trees & Graphs: Traversal & Backtracking

You don't need to know the right path from the start. You just need a system to explore, retreat, and try again.

Many problems can't be solved by a simple, linear algorithm. Instead, they require you to explore a vast space of possibilities, like finding every possible subset of a collection or every valid move on a chessboard. This is what backtracking is all about.

Backtracking is not a specific algorithm, but a technique. It's a methodical way of exploring a "decision tree" of choices.

Problem: Subsets (LeetCode Link): Given an integer array nums of unique elements, return all possible subsets (the power set). 💡 Hint

The Core Pattern: Choose, Explore, Unchoose

Think of building a solution as making a sequence of choices. Backtracking formalizes this with a recursive template:

  1. Choose: Make a choice and add it to your current "path" or candidate solution.
  2. Explore: Recursively call your function to explore all subsequent choices that can follow from the one you just made.
  3. Unchoose: After the recursive call returns (meaning it has fully explored that branch), undo your initial choice. This is the "backtrack" step. It's what allows you to explore a completely different branch of the decision tree.

Walkthrough: Generating All Subsets

Let's apply this pattern to the "Subsets" problem. Our "decision tree" involves deciding, for each number, whether to include it in the current subset.

Our recursive function, backtrack(startIndex, currentSubset), will work as follows:

  1. First, add the currentSubset to our list of all results. This captures the subset at the current state.
  2. Iterate from startIndex to the end of the nums array. In each iteration, we...
    • Choose: Add nums[i] to currentSubset.
    • Explore: Recursively call backtrack(i + 1, currentSubset). We pass i + 1 to ensure we only consider numbers that appear after the current one, preventing duplicate combinations.
    • Unchoose: After the recursive call returns, currentSubset.pop(). This removes nums[i], allowing us to explore the next branch where nums[i] is not included.
def subsets(nums: list[int]) -> list[list[int]]:
    result = []
    path = []

    def backtrack(start_index):
        # Add the current subset path to the results
        result.append(path.copy())

        # Explore all choices from the current start_index
        for i in range(start_index, len(nums)):
            # 1. Choose
            path.append(nums[i])
            # 2. Explore
            backtrack(i + 1)
            # 3. Unchoose (Backtrack)
            path.pop()

    backtrack(0)
    return result

More Problems & Variations

The same fundamental "Choose, Explore, Unchoose" template can solve a wide range of problems with minor tweaks.

  1. Combinations (LeetCode Link): Given two integers n and k, return all possible combinations of k numbers chosen from the range [1, n]. 💡 Hint

  2. Permutations (LeetCode Link): Given an array of distinct integers, return all possible permutations. 💡 Hint

  3. Combination Sum (LeetCode Link): Find all unique combinations of numbers that sum up to a target. 💡 Hint

Bonus Points: Constraint Programming

Backtracking is the core engine behind a field called Constraint Programming. It's used to solve complex scheduling problems (like "What's the most efficient flight schedule given a set of planes, pilots, and routes?"), resource allocation, and classic puzzles like Sudoku or the N-Queens problem. The algorithm explores the vast tree of possible choices, but "prunes" entire branches as soon as a choice violates one of the problem's constraints.