Coding Pattern:

Backtracking

This pattern has an earnable cheat sheet.

Overview

Backtracking is a technique to recursively build a solution incrementally. You can think of it as a way to search through all possible solutions (almost akin to brute force with a few constraints).

There are two main categories of problems that backtracking solves:

  1. Combinatorial Problems involve returning a combination or permutation of a given input. This is the most common case of backtracking.

    Example: Find all possible dice rolls to sum to a given target.

  2. Validation Problems involve figuring out if a given statement is true or not. These questions are less common and can sometimes be tricky to notice that they involve backtracking.

    Example: Determine if it is possible to find a word in a word search grid.

Note

If you ever see the words "combination" or "permutation" or even "subsets", it's likely that you'll need to use a form of backtracking.

Pattern concept

To demonstrate backtracking, let's start with a simple example: Get all subsets from a list of unique elements.

Step 1 of 32
Backtracking Slide 1: A subset of a list is a collection of all or some elements from a list where they f... are all subets of the list...
Backtracking Slide 2: Let's look at a decision tree for the backtracking algorithm.
Backtracking Slide 3: nums = [5, 9, 7]
Backtracking Slide 4: Take 5? nums = [5, 9, 7]
Backtracking Slide 5: Take 5? Take 9? nums = [5, 9, 7]
Backtracking Slide 6: Take 5? Take 9? Take 7? nums = [5, 9, 7]
Backtracking Slide 7: Take 5? nums = [5, 9, 7] Take 9? Take 7?
Backtracking Slide 8: Now for the details of the backtracking algorithm...
Backtracking Slide 9: First initialize a result list and a current list.
Backtracking Slide 10: Starting at index 0 of nums, we choose to take the value and append it to curr.
Backtracking Slide 11: Recurse into index 1. We also choose to take this value and append it to curr. f(0)
Backtracking Slide 12: Recurse into index 2. We choose to take the value and append it to curr. f(0) f(1)
Backtracking Slide 13: Recurse to index 3. Since we reached the end of nums, append curr to the result list... f(0) f(1) f(2)
Backtracking Slide 14: Back at index 2, this time we don't take this value. Pop it from curr. f(0) f(1)
Backtracking Slide 15: Recurse into index 3. Since we are at the end of nums, append curr to the result lis... f(0) f(1) f(2)
Backtracking Slide 16: Going back to f(1), this time we don't take this value. Pop it from curr. f(0)
Backtracking Slide 17: Recurse into f(2). We decide to take this value and append it to curr. f(0) f(1)
Backtracking Slide 18: Recurse into f(3). Since we reached the end of nums, append curr to the result list. f(0) f(1) f(2)
Backtracking Slide 19: Back at f(2), this time we don't take this value. Pop it from curr. f(0) f(1)
Backtracking Slide 20: Recurse into f(3). Since we're at the end of nums, append curr to the result list. f(0) f(1) f(2)
Backtracking Slide 21: f(1) and f(2) are resolved. Back in f(0), this time we decide to not take the value....
Backtracking Slide 22: Recurse into f(1). We first decide to take the value and append it to curr. f(0)
Backtracking Slide 23: Recurse into f(2). We first decide to take the value and append it to curr. f(0) f(1)
Backtracking Slide 24: Recurse into f(3). We reached the end of nums, append curr to the result list. f(0) f(1) f(2)
Backtracking Slide 25: Going back to f(2), this time we decide to not take this value. Pop curr. f(0) f(1)
Backtracking Slide 26: Recurse into f(3). We reached the end of nums so append curr to the result list. f(0) f(1) f(2)
Backtracking Slide 27: f(2) is resolved. So back at f(1), this time we don't take this value. Pop curr. f(0)
Backtracking Slide 28: Recurse into f(2). Take the value at index 2 and append it to curr. f(0) f(1)
Backtracking Slide 29: Recurse into f(3). We reached the end of nums. Append curr to the result list. f(0) f(1) f(2)
Backtracking Slide 30: Going back to f(2), this time we don't take the value. Pop it from curr. f(0) f(1)
Backtracking Slide 31: f(0) f(1) Recurse into f(3). We reached the end of nums so append curr to the result list. f(2)
Backtracking Slide 32: f(0) is resolved, so the recursive helper function exits. Return the result list!

Use ← → arrow keys

Solution template

# === Recursive Helper Function ===
def helper(self, i: int, curr: List[int], nums: List[int], res: List[List[int]]):
  # === Valid Result Check ===
  if i == len(nums):
    res.append(curr.copy())
    return
  
  # === Decision Tree ===
  curr.append(nums[i])  # To take...
  self.helper(i + 1, curr, nums, res)
  curr.pop()  # Or not to take...
  self.helper(i + 1, curr, nums, res)

# ==== Original function ====
def get_subsets(nums: List[int]):
  # === Result List Initialization ===
  res = [] 
  helper(0, [], nums, res)
  return res
  
print(get_subsets([5, 9, 7]))
    
main.py

There are two main steps to solve a backtracking question:

  • Valid Result Check

    Make sure that the current possible combination is valid.

    A common example to do this is checking if we reached the end of the input list. A less common example is checking if a set of parenthesis are balanced.

    Note

    Be careful when adding the current list to the result list. Make sure to add a copy of the current list, otherwise you will end up modifying that list later in the algorithm.

  • Decision Tree

    Backtracking brute forces finding all possible combinations. This is the part where we incrementally build a possible solution. It can include these decisions:

    • Take the element and recurse further into the branch.
    • Don't take the element (remove it if it was already added).
    • Try a different possible element by recursing into a different branch.

Besides the core code segments there are a few other auxiliary code segments to mention:

  • Recursive Helper Function

    All backtracking questions require recursion. Not only that, all backtracking questions require a few necessary parameters:

    1. The current index that you are on
    2. The current solution you are building
    3. The input set
    4. The result list of all solutions (combinatorial problems) or a boolean result (validation problems)

  • Result List Initialization (Combinatorial Problems Only)

    The code for this section will always be the same. Initialize an empty result list and pass it to your recursive function. This list will then be populated with the valid combinations once the backtracking process is done.

    Note

    If you're asked for combinations ([[1,2,3], [3, 2, 1]]), return the result list. If you're asked for number of combinations (2 combinations), return the result list's length.

How to identify

Does the problem involve any of these?

  • Collecting a set of combinations / permutations / partitions of a given input
    • Get all subsets from a list of unique elements.
    • Letter Combinations of a Phone Number: Find all letter combinations of a given phone number.
    • Combination Sum: Find all unique combinations of numbers in a given list that sum up to a specific target.
    • N Queens: Generate all possibilities where you can place n queens on a chessboard in a way where no two queens threaten each other.
  • A validation problem (true / false) involving the incremental building of a solution
    • Combination Sum (Variant): Check if you can add to a target number using n numbers.
    • Word Search: Determine whether a given word can be formed by connecting adjacent letters in a grid without revisiting a letter.

Note

Backtracking is a brute force solution. It often involves deciding between a binary choice of either taking an element, or not taking it. In other words, every additional element in the input list doubles the solution space. This means that the time complexity is usually O(2n).