Skip to content

Intermediate: core algorithms · Recursion and backtracking · topic 23 of 61 · 40 min

Backtracking: subsets and permutations

Explanation

Backtracking builds every possible answer one choice at a time: choose an option, explore everything that follows from it, then undo the choice and try the next one. The choices form a decision tree. Generating all subsets visits 2ⁿ leaves and all permutations n! leaves, so backtracking is for small n (about 20 for subsets, about 10 for permutations).

When you need every answer

Some questions ask for all possibilities: every team you can form from five players, every order in which a delivery rider can visit four addresses, every way to fill a timetable. There is no shortcut to list them all; you must generate them. Backtracking is the clean, standard way to do it with recursion, without missing any and without listing any twice.

Choose, explore, un-choose

Keep a path: the choices made so far (a list). At each step, loop over the options that are still allowed. For each one: choose it (append it to the path), explore (call the function recursively to make the remaining choices), and un-choose it (pop it from the path) so the next option starts from the same state. When the path is complete, save a COPY of it. The undo step is what gives backtracking its name: you walk back up the tree and try another branch.

The decision tree

Draw every state of the path as a node. The empty path is the root; each choice is an edge to a child. For subsets of [1, 2, 3], each level decides one number: take it or skip it. That gives 2 × 2 × 2 = 8 leaves, one per subset. Backtracking is simply a depth-first walk over this tree, which is why the stack depth is only n.

output
                 []
          take 1/      \skip 1
           [1]            []
        take 2/ \skip   take 2/ \skip
      [1,2]    [1]      [2]     []
       / \     / \      / \     / \
 [1,2,3][1,2][1,3][1] [2,3][2] [3] []
Each level decides one number. The 8 leaves are the 8 subsets.

All subsets: 2ⁿ of them

A common way to write it: at each call, the current path is itself a subset, so save a copy; then try adding each later number, starting from index start so that every subset is built in one order only ([1, 2], never [2, 1] as well).

python
def subsets(nums):
    result, path = [], []

    def backtrack(start):
        result.append(path[:])            # save a COPY of the current subset
        for i in range(start, len(nums)):
            path.append(nums[i])          # choose
            backtrack(i + 1)              # explore
            path.pop()                    # un-choose

    backtrack(0)
    return result

print(subsets([1, 2, 3]))
# Output: [[], [1], [1, 2], [1, 2, 3], [1, 3], [2], [2, 3], [3]]
There are 2ⁿ subsets and copying each costs up to n, so O(n · 2ⁿ) time; the recursion is O(n) deep.

All permutations: n! of them

A permutation is an ordering of all the items. The first position has n choices, the next n − 1, and so on: n × (n − 1) × … × 1 = n! orderings. Keep a used list of booleans; at each level try every unused item.

python
def permutations(nums):
    result, path = [], []
    used = [False] * len(nums)

    def backtrack():
        if len(path) == len(nums):
            result.append(path[:])
            return
        for i in range(len(nums)):
            if used[i]:
                continue
            used[i] = True; path.append(nums[i])     # choose
            backtrack()                              # explore
            path.pop(); used[i] = False              # un-choose

    backtrack()
    return result

print(permutations([1, 2, 3]))
# Output: [[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]]
O(n · n!) time, O(n) extra space besides the output.

Handling duplicates: sort, then skip

With nums = [1, 2, 2], the code above makes the subset [2] twice, once with each 2. The fix: sort nums first so equal values sit together, and inside the loop skip nums[i] when i > start and nums[i] == nums[i − 1]. In words: at one level of the tree, use each distinct value only once as the next choice. Going deeper (i + 1) can still take the second 2, so [2, 2] is still made, exactly once. Permutations with duplicates use the same idea: skip nums[i] if it equals nums[i − 1] and nums[i − 1] is not currently used.

How big can n be?

n2ⁿ (subsets)n! (permutations)
532120
101,0243,628,800
1532,768about 1.3 × 10¹²
20about 10⁶about 2.4 × 10¹⁸

Python manages roughly 10⁶ to 10⁷ simple steps per second, so subsets are fine up to about n = 20 and permutations up to about n = 9 or 10. When an interview problem says n ≤ 15 or n ≤ 20, it is often hinting at backtracking. Python's itertools.combinations and itertools.permutations generate these too, but interviewers usually want to see you write the backtracking.

Common mistakes

  • Saving the path itself (result.append(path)) instead of a copy: every saved entry is the same list, which ends up empty. Fix: result.append(path[:]) or list(path).
  • Forgetting to un-choose (pop, or reset used[i]), so choices leak into the next branch. Fix: every append before the recursive call needs a pop after it.
  • Starting the subset loop at 0 instead of start, which creates [1, 2] and [2, 1] as different subsets. Fix: pass i + 1 down.
  • Skipping duplicates without sorting first, or with i > 0 instead of i > start, which wrongly drops [2, 2]. Fix: sort, then use i > start.
  • Running backtracking on n = 30 and waiting forever. Fix: check 2ⁿ or n! against the limits before you start.

Need help?

Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.

Try:

Watch the lesson

Backtracking: subsets and permutations · 1/8

Backtracking

This lesson introduces backtracking: building every possible answer one choice at a time, and undoing choices to try the next.

Prefer a video by a teacher? Find free videos on “backtracking subsets permutations decision tree explained python” on YouTube ↗

In real life: Trying every combo offer in a food delivery cart

A food app may want to check which add-ons a customer could pick to reach a "free delivery" amount. With a handful of add-ons, the app can simply try every subset of them and keep those whose total is high enough. With 5 add-ons that is only 32 subsets. With 40 items it would be over a trillion, which is why real systems use smarter methods (dynamic programming in Module 11, or greedy rules) for large menus.

python
addons = [("Raita", 40), ("Gulab jamun", 60), ("Coke", 45)]
need = 100
best = []

def backtrack(i, path, total):
    if i == len(addons):
        if total >= need:
            best.append([name for name, _ in path])
        return
    path.append(addons[i])                    # take it
    backtrack(i + 1, path, total + addons[i][1])
    path.pop()                                # skip it
    backtrack(i + 1, path, total)

backtrack(0, [], 0)
print(best)
Shows [['Raita', 'Gulab jamun', 'Coke'], ['Raita', 'Gulab jamun'], ['Gulab jamun', 'Coke']]. Raita and Coke alone make only 85.

Practice (0/4 done)

Task 1 · Code · Python

Write subsets(nums) that returns a list of ALL subsets of nums (the values are distinct). Each subset is a list; the empty subset is included. The order of the subsets and of the items inside each subset does not matter (the tests sort them before comparing). Use backtracking; do not use itertools. Example: subsets([1, 2]) returns [[], [1], [1, 2], [2]] in any order.

Task 2 · Code · Python

Write permutations(nums) that returns a list of ALL orderings of nums (the values are distinct). Each ordering is a list. The order of the orderings does not matter (the tests sort the list), but each inner list must keep its order. The empty list has exactly one ordering, []. Use backtracking with a used list; do not use itertools. Example: permutations([1, 2]) returns [[1, 2], [2, 1]].

Task 3 · Code · Python

Write subsets_with_dup(nums) where nums MAY contain duplicates. Return all DIFFERENT subsets: two subsets are the same if they hold the same values the same number of times. Each subset is a list; the order of subsets and of items inside them does not matter (the tests sort before comparing), but no subset may appear twice. Sort nums first and skip equal values at the same level. Example: subsets_with_dup([1, 2, 2]) returns [[], [1], [1, 2], [1, 2, 2], [2], [2, 2]] in any order.

Task 4 · Code · Javascript

Write binaryStrings(n) in JavaScript. Return an array of ALL strings of length n made of the characters "0" and "1", in lexicographic (dictionary) order. Use backtracking: try "0" first, then "1", at each position. binaryStrings(0) returns [""] (one empty string). Example: binaryStrings(2) returns ["00", "01", "10", "11"].

Quick quiz

Question 1

1.Why does backtracking save path[:] instead of path?

Question 2

2.How many subsets does a set of 6 distinct items have?

Question 3

3.How many permutations do 5 distinct items have?

Question 4

4.In subsets with duplicates, why is the skip test i > start and not i > 0?

Question 5

5.Which is the correct order inside a backtracking loop?

Read more (free)

Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.

Why does backtracking save path[:] instead of path?

Answer: Without a copy, every saved entry is the same list, which keeps changing and ends empty

result.append(path) stores a reference to one list; later pops change every "saved" entry.

How many subsets does a set of 6 distinct items have?

Answer: 64

Each item is in or out: 2⁶ = 64, including the empty set.

How many permutations do 5 distinct items have?

Answer: 120

5! = 5 × 4 × 3 × 2 × 1 = 120.

In subsets with duplicates, why is the skip test i > start and not i > 0?

Answer: i > 0 would also skip the second 2 when going deeper, losing subsets such as [2, 2]

Only repeats at the SAME level are duplicates. Deeper levels start at start = i + 1 and may use the equal value once.

Which is the correct order inside a backtracking loop?

Answer: Choose, explore, un-choose

Make the choice, recurse with it in place, then undo it before trying the next option.

What does pruning do in backtracking?Related

Answer: It stops exploring a branch as soon as it cannot lead to a valid answer

Pruning returns early from hopeless partial paths, so whole subtrees are never built.

From Backtracking with pruning

How many masks do you loop over to list every subset of 4 items?Related

Answer: 16

2⁴ = 16 masks: 0 to 15.

From Bit tricks in interviews

Target sum: numbers [1, 1, 1, 1, 1], target 3, each with + or −. Which subset sum P do you count?Related

Answer: P = 4

P = (total + target) / 2 = (5 + 3) / 2 = 4. There are 5 ways to choose four 1s.

From Knapsack problems

Why do random forests choose random feature subsets at each split?Related

Answer: To make the trees different from each other so their errors cancel

Diverse trees give a stronger combined vote.

From Random forests and bagging

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

Next steps

  1. 2 more topics in “Recursion and backtracking” before its exam: Backtracking with pruning, Memoization: remembering answers.
  2. Module 5 exam: Recursion and backtracking · 14 questions · 25 min, without help. 5 topics of this module still to finish first.
  3. Then the next module, Sorting and searching. 61 topics left in the course before the final exam.

← Previous: Divide and conquer