AlgoAlgo/05 · Backtracking
CHAPTER 05 · Backtracking

Backtracking search

Backtracking builds an answer one choice at a time. At each step it takes an allowed choice and goes deeper. When the current partial answer cannot be finished, or has already been recorded, it removes the last choice it made and tries the next one. That removal is the "backtrack", and it is the only thing that separates this from ordinary recursion. This chapter turns any "list every solution" problem into three parts — the path, the choices available now, and the stop condition — and then uses pruning to cut branches that cannot lead anywhere.

§01

Why backtracking: enumeration that nested loops cannot write

When the number of steps and the choices at each step are decided during the search, a fixed stack of for loops no longer works.

Start with an enumeration a loop can handle: choose 2 numbers out of 5. Two nested for loops do it. Now change the problem to choose k out of n, where k is only known when the program runs. You cannot write "k nested loops", because the number of loop levels is fixed when you write the code. The same difficulty appears in "cut a string into any number of pieces" and "place queens until the board is full". These problems share one property: how many choices you make, and which choices are legal at each step, are decided while the search runs.

Here is the method, before it gets a name. You keep one partial answer. At each step you list the choices that are still allowed, take one, add it to the partial answer, and continue from there. When the partial answer is complete you record it. When it cannot be completed, or after you have recorded it, you remove the last choice you added and try the next choice at that same step. When a step runs out of choices, you return to the step before it and do the same thing there.

Draw every partial answer as a node and every choice as an edge, and you get a tree. This method is a depth-first search over that tree of partial answers, and the step that removes the last choice is called backtracking. That undo step is the whole difference between backtracking and ordinary recursion: it restores the shared partial answer to exactly what it was before this node was entered, so the next branch starts from a clean state. The tree is usually called a decision tree. It is never built in memory — it exists only as the shape of the recursion.

Walking a maze: try one path, and when it hits a wall step back and take another
StartLeftWallRightExit
You are standing at the start, and two paths lead away from it. Backtracking does the plain thing: pick one and try it. Go left first.
1 / 6

One step forward and one step back — that is the entire template: choose → recurse → un-choose. Every remaining problem in this chapter differs only in two things: what the decision tree looks like, and which branches can be cut before you enter them.

Signal 01
It asks for all solutions

The problem says "return every combination / permutation / partition / path", not "return the best value". That is almost always backtracking, or dynamic programming when the same subproblem repeats and only a count or an optimum is needed.

Signal 02
The number of steps is not fixed

"Choose k", "cut into some number of pieces", "fill the board" — the number of steps is a variable, so you cannot write that many nested loops. Recursion supplies one level per step.

Cost warning
Exponential

Backtracking enumerates, so the cost is the number of nodes in the search tree times the work per node. For all subsets of n elements the tree has about O(2ⁿ) nodes; for all permutations of n elements it has about n! leaves. Pruning is not a finishing touch here — it often decides whether the solution runs in time.

Where the name comes from

The word backtracking was introduced by the mathematician D. H. Lehmer around 1950, but the idea is much older: the eight queens puzzle was posed in 1848, and Gauss worked on it by hand. Backtracking and DFS are closely related but not identical. DFS is a way to traverse a graph or tree that already exists. Backtracking is DFS over a tree of partial solutions that is generated while you search, with an undo step at every node. Later in this chapter you will see the same mechanism inside Prolog, regular expression engines, and sudoku solvers.

§02

The template: path, choices, stop condition

Answer these three questions for any backtracking problem and the template writes itself.

Backtracking problems look very different from each other, but they share one skeleton. Before writing code, answer three questions:

Question 1
The path

The choices you have already made, kept in one list. It is the route from the root to the current node of the decision tree. When you reach a node that counts as an answer, a copy of this list is that answer.

Question 2
The choices available now

What this step is still allowed to pick. Combinations and subsets use a startIndex that only moves forward; permutations use a used array; a board uses "is this square attacked?".

Question 3
The stop condition

When the current path counts as a finished answer: k numbers chosen, the string fully cut, n rows filled. Record it and return.

With those three answers, fill in the skeleton below. The example is "choose k numbers out of 1..n". Look at the three lines inside the loop — choose, recurse, un-choose — and note that the un-choose is the exact reverse of the choose.

backtrack_template.py
1res, path = [], []
2
3# Skeleton: choose k numbers out of 1..n
4def backtrack(n, k, start):
5 if len(path) == k: # stop condition
6 res.append(path[:]) # record: store a copy
7 return
8 for i in range(start, n + 1): # choices available now
9 path.append(i) # 1. choose
10 backtrack(n, k, i + 1) # 2. go one level deeper
11 path.pop() # 3. un-choose
Most common mistake: record path[:] or path.copy(). res.append(path) appends a reference to the same list, and the later pop calls empty it — that is why beginners end up with a result full of empty lists.

The un-choose must exactly reverse the choose

Many people write the choose and the recursion correctly and forget the un-choose. The result is not a slow program, it is a wrong one. All branches share the same path object. Without the undo, a sibling branch continues on top of what the previous branch left behind. The rule is simple: whatever the choose changed, the un-choose changes back. If the choose touched two pieces of state — for example path and used[i] — the un-choose must restore both. This is the single most common backtracking bug.

Two ways to carry the path

There is a second, also correct, way to write this: give each recursive call a fresh copy of the path with the new element already appended, and never undo anything. It is shorter and it removes the whole class of bugs above. It also costs more: every node copies the path, which is O(path length) extra time and memory per node, not just per solution. The code in this chapter mutates one shared list and undoes the change, because the undo is O(1) and nothing is allocated on the way down. Copying is then needed only at the moments when a solution is recorded.

In an interview: say the three parts out loud

Do not start typing. Say this first: "I will model it as a decision tree. The path holds the choices made so far. The choices available now are controlled by a startIndex here, or by a used array. The stop condition is that the path has length k. Then the loop body is choose, recurse, un-choose, and finally I will look for branches I can prune." That shows a framework you can reuse, not one memorised problem.

§03

Featured problem A · LC 77 Combinations: draw the tree, then cut it down

MEDIUM

Combinations are the first backtracking problem to learn, and they teach the two ideas this chapter is built on: a start index, and pruning before you enter a branch.

Problem: given integers n and k, return every combination of k numbers from 1..n. Order does not matter, so [1,2] and [2,1] are the same combination. Brute force: if k were a constant you could write k nested loops, but k is a parameter. Solution: build a decision tree. Level 1 picks the first number, level 2 picks the second number from those larger than the first, and so on. Every node whose path has length k is one combination.

Here is the first key point of the combination family: why can the next level only pick a larger number? Because a combination is a set, not a sequence. If every level could pick any number, the search would produce both [1,2] and [2,1] — the same set twice. A startIndex that only moves forward forces every combination to appear exactly once, in increasing order. First, the full tree with no pruning (n=4, k=2):

LC 77 Combinations · no pruning: even the hopeless branch starting with 4 is visited
1234234344
Goal: choose 2 numbers from {1,2,3,4}. The path is empty and the loop starts at 1.
1 / 12

Count the green leaves: exactly 6 = C(4,2). Now look at the branch that starts by picking 4. You still need 2 numbers in total, but you started at the last one, so no larger number is left and that branch can never produce a complete answer. The search entered it anyway and only found out at the bottom. This is what pruning fixes: before entering a subtree, check whether it can still lead to a valid answer, and skip it if it cannot.

For LC 77: you still need k − path.size() more numbers, so the starting value i can be at most n − (k − path.size()) + 1; beyond that there are not enough numbers left. Tighten the upper bound of the loop from n to that value and the dead branch is never entered at all:

LC 77 Combinations · with pruning: the branch starting with 4 is never entered (grey)
1234234344
The same empty path, but now the loop limit is computed first: 2 more numbers are needed and 4 are available, so the first pick can go no further than 3. Starting at 4 would leave nothing after it, so that node stays grey the whole way: the search never enters it.
1 / 11
lc77_combine.py
1class Solution:
2 def combine(self, n: int, k: int) -> list[list[int]]:
3 res, path = [], []
4
5 def backtrack(start: int) -> None:
6 if len(path) == k:
7 res.append(path[:]) # store a copy
8 return
9 last = n - (k - len(path)) + 1 # pruning bound
10 for i in range(start, last + 1):
11 path.append(i)
12 backtrack(i + 1)
13 path.pop()
14
15 backtrack(1)
16 return res
Do not drop the + 1 in range(start, last + 1): Python's range excludes its upper end, so reaching last requires last + 1. Derive the bound once by hand: k − len(path) numbers are still missing.

Two kinds of pruning

1. This branch cannot be valid — a constraint check. You prove that no complete, legal answer exists below this node, so you skip it. The bound on i above is this kind, and so is the palindrome test in LC 131 and the "is this square attacked?" test in LC 51. It never removes a real answer.

2. This branch cannot be better — a bound. This one only applies when you want the best answer rather than all answers. You keep the best score found so far, compute an optimistic estimate of the best score still reachable below this node, and skip the node when the estimate cannot beat the current best. That method is called branch and bound. Everything in this chapter enumerates all answers, so every example here uses the first kind.

Complexity and follow-up questions

Count it as nodes in the search tree times work per node. The tree has C(n,k) leaves; interior nodes are fewer, and each does O(1) work per child. Each leaf copies a path of length k. So time is O(k · C(n,k)), where n is the size of the range and k is the size of one combination. Space is O(k) for the path and the recursion, not counting the output. Pruning removes branches that produce nothing, which can be a large speed-up, but it can never go below the cost of writing out C(n,k) answers.

Follow-ups: (1) "the numbers must also sum to a target" → LC 216, add one more constraint check on the running sum; (2) "the same number may be reused" → LC 39, recurse with i instead of i + 1; (3) "the array contains duplicates and each element is used once" → LC 40, sort first and skip duplicates at the same level (§08).

§04

The combination family: one skeleton, five variations

LC 17 / 216 / 39 / 40 are LC 77 plus one extra rule each. The trick is to see exactly which line changes.

Almost every combination problem adds a condition to the LC 77 skeleton. Once you see which single part changes, you stop mixing them up:

LC 17 · Letter combinations
One pick from each of several sets

You do not pick from one set. Each level uses a different set (digit 2 → "abc", 3 → "def", …). So there is no startIndex; an index says which digit, and therefore which set, this level uses. Stop when every digit has been used.

LC 216 · Combination Sum III
Count and sum, two constraints

LC 77 plus a running sum: a path of length k is recorded only if sum equals the target. Two constraint checks stack up — not enough numbers left, or the sum already exceeds the target. The second one is valid because 1..9 are all positive, so the sum can only grow.

LC 39 · Combination Sum
The same number may be reused

Elements are distinct but each may be used any number of times. One change: recurse with i instead of i + 1, so the next level can pick the same element again. Sort first, then break as soon as the sum would exceed the target.

LC 40 · Combination Sum II
Duplicates, each used once

The array contains duplicate values, each element may be used once, and the result must contain no duplicate combinations. Sort, then skip duplicates at the same level: i > start && nums[i] == nums[i-1]. Recurse with i + 1. §08 explains why this is correct.

LC 22 · Generate Parentheses
The constraint is the pruning

Each level has two choices: write "(" or write ")". The rules are the pruning: you may write "(" while fewer than n have been written, and ")" only while the number of ")" is less than the number of "(". Nothing else is needed.

The summary
Which line changes

Reuse allowed? → pass i or i+1. One set or several? → startIndex or a level index. Duplicate values? → sort and skip at the same level. Extra rule? → one more constraint check.

§05

Partitioning: a cut is also a choice

LC 131 / 93 — startIndex stops meaning "which number" and starts meaning "where the next cut goes".

Cutting a string into pieces does not look like choosing numbers, but it is the same tree. One sentence is enough: read startIndex as "the first character of the next piece". At each node you try every cut point i from start to the end of the string; the substring s[start..i] is the piece you choose at this step. Reaching the end of the string means the whole string has been cut, which is one complete answer.

LC 131 · Palindrome Partitioning
Every piece must be a palindrome

dfs(start): try each cut point i. If s[start..i] is a palindrome, push it, recurse with dfs(i+1), then pop. If it is not a palindrome, skip that cut point — that is the constraint check. start at the end of the string means one complete partition. Precomputing a palindrome table makes the test O(1).

LC 93 · Restore IP Addresses
More rules means more pruning

The same partitioning skeleton with more rules: exactly 4 pieces, each piece between 0 and 255, and no leading zero except the single digit "0". Track the number of pieces as a second dimension — four pieces is the stop condition — and skip any piece that breaks a rule.

What the recursion may reuse: the organising idea

Combinations, partitions, subsets, and permutations all run on the same skeleton. The one thing that differs is what a deeper call is allowed to reuse:

Subsets, combinations, partitions move a startIndex forward, so a deeper call can only use elements after the one just taken. Order is fixed, and each set of elements is produced once. Permutations may go back to any element that is not currently on the path, so they track a used set instead. Everything else — the three questions, the three steps, the copy when recording — is identical.

§06

Featured problem B · LC 78 Subsets: record at every node

MEDIUM

Same tree as combinations. The only difference is when a node counts as an answer.

Problem: return all subsets of an array whose elements are distinct, including the empty set and the array itself. Key idea: for combinations a node counts only when the path has length k. For subsets the path itself is already an answer — the empty set, {1}, {1,2}, all of them. So subsets and combinations share the same startIndex tree, and differ in one thing only: when you record.

Combinations record at the leaves. Subsets record at every node, on entry, before the loop starts, because the path that leads to any node is itself a valid subset. In the animation every node turns green as it is recorded:

LC 78 Subsets · one answer is recorded at every node (green = already recorded)
1233233
The empty set {} is a valid subset, so record it first, then start picking from 1.
1 / 9
lc78_subsets.py
1class Solution:
2 def subsets(self, nums: list[int]) -> list[list[int]]:
3 res, path = [], []
4
5 def backtrack(start: int) -> None:
6 res.append(path[:]) # record at every node
7 for i in range(start, len(nums)):
8 path.append(nums[i])
9 backtrack(i + 1)
10 path.pop()
11
12 backtrack(0)
13 return res
There is no "path is full" return here; the first line of the function records a copy of the current path. An equivalent formulation is a binary "take it or skip it" tree, but the startIndex version matches the rest of this chapter.

Complexity and follow-up questions

The tree has 2ⁿ nodes, where n is the number of elements, and every node copies a path of length at most n. Time is O(n · 2ⁿ), space is O(n) for the path and the recursion, not counting the output, which is itself O(n · 2ⁿ). Follow-ups: (1) "duplicate values?" → LC 90, sort and skip at the same level (§08); (2) "increasing subsequences only, and sorting is not allowed?" → LC 491, use one set per level instead; (3) "without recursion?" → enumerate the integers 0..2ⁿ−1 and read bit j as "element j is included" (chapter 04 covers using one integer as a set).

§07

Featured problem C · LC 46 Permutations: startIndex fails, use a used array

MEDIUM

Order matters here, so [1,2] and [2,1] are two different answers. Every level must be able to reach back.

Problem: return all permutations of an array whose elements are distinct. The difference from combinations: order matters, so [1,2] and [2,1] are two different answers. That makes the "only move forward" startIndex wrong here: in [2,1] the 1 comes after the 2, so a forward-only search can never build it, and most of the answers go missing.

A permutation needs every level to reach every element that is not already on the path, including smaller ones. How do you know which ones are on the path? Keep a boolean array called used: used[i] = true means element i is currently on the path, and this level skips it. Watch used light up on a choose and go dark on an un-choose:

LC 46 Permutations of [1,2,3] · the used array, step by step (green = on the path · yellow = just picked · red = already used)
path
nums / used
1F
2F
3F
Take 1: append it to the path and set used[0] to true.
1 / 52

Notice that the un-choose restores two pieces of state, used and path. That is the extra trap permutations add on top of combinations. The loop starts from i = 0 at every level, never from a start index, and used does the filtering:

lc46_permute.py
1class Solution:
2 def permute(self, nums: list[int]) -> list[list[int]]:
3 res, path = [], []
4 used = [False] * len(nums)
5
6 def backtrack() -> None:
7 if len(path) == len(nums):
8 res.append(path[:])
9 return
10 for i in range(len(nums)): # every level starts at 0
11 if used[i]:
12 continue # skip what is on the path
13 used[i] = True; path.append(nums[i])
14 backtrack()
15 used[i] = False; path.pop() # undo both changes
16
17 backtrack()
18 return res
A shorter Python version writes for x in nums: if x in path: continue and drops used. It is correct only when the values are distinct, and in scans the list in O(n) while used[i] is O(1). Prefer used.

Two mistakes people make with permutations

(1) Copying the startIndex from combinations. Then [2,1] and [3,1] are never produced. That is missing answers, not slow code. Every level of a permutation must scan from 0. (2) Undoing path but forgetting used. One element stays marked as taken forever, and every permutation that needed it disappears. Remember that the undo here has two halves.

Complexity and follow-up questions

The tree has n! leaves, where n is the number of elements; interior nodes add a constant factor. Each leaf copies a path of length n, so time is O(n · n!) and space is O(n) for the path, used, and the recursion, not counting the output. Follow-ups: (1) "duplicate values?" → LC 47, sort and add !used[i-1] (§08); (2) "permutations of a string?" → the same used array over characters; (3) "can you avoid the used array?" → yes, by swapping elements in place to generate permutations, which saves the array but is harder to read and does not handle duplicates by itself.

§08

Duplicate values: skip at the same level, keep along the path

Frequent interview topic

When the input contains repeated values, how do you keep the answers unique? This is the part people get wrong most often.

The problems so far (77 / 78 / 46) all assumed distinct elements. As soon as a value repeats — for example [1,1,2] — duplicate answers appear. Look at the symptom first: all subsets of [1,1,2], with no special handling:

Subsets II · no deduplication: {1} and {1,2} are each produced twice
1122122
The array [1, 1, 2] holds two 1s. Do nothing special at first and enumerate every index. The empty set is recorded.
1 / 9

{1} appears twice and so does {1,2}. The cause, in one sentence: inside the same loop, the second 1 opened a branch identical to the one the first 1 already opened. The fix is to sort first, so equal values sit next to each other, and then, inside the loop, skip a value that equals the previous one. With that rule the duplicate branch is never entered:

Subsets II · skip at the same level: the duplicate branch is never entered (grey)
1122122
Sort first (this array is already sorted). The rule in one line: inside one loop, skip a value equal to the previous one i > start && nums[i] == nums[i-1]. The empty set is recorded. The two grey nodes are the branch that rule will skip.
1 / 7

For combinations and subsets, which have a startIndex, the rule is written i > start && nums[i] == nums[i-1]. Note that it is i > start, not i > 0:

Here is why it is correct. After sorting, equal values are adjacent. At one node the loop tries index start, start+1, … as the next element of the path. Two indexes holding the same value leave the same remaining choices below them, so they generate identical subtrees: every answer found under the second one was already found under the first. i > start keeps the first index of each run of equal values at this node and skips the rest, so each distinct value is tried once per node.

And here is why it does not remove valid answers. The condition only compares an index with its sibling in the same loop. i == start is the first choice at this node and always passes. A deeper call receives start = i + 1, so a value equal to the one just taken arrives at the deeper node at position start and passes the test there. That is exactly why {1,1} survives while a second {1} branch does not. Writing i > 0 instead would also compare across levels and would delete {1,1}.

lc90_subsets_ii_dedup.py
1# call nums.sort() first, so equal values are adjacent
2def backtrack(start):
3 res.append(path[:]) # subsets: record at every node
4 for i in range(start, len(nums)):
5 if i > start and nums[i] == nums[i - 1]: # same level, same value
6 continue # skip the repeat
7 path.append(nums[i])
8 backtrack(i + 1)
9 path.pop()
Sorting is a precondition. Without it, equal values are not adjacent and "compare with the previous element" proves nothing. LC 491 must keep the original order, so it cannot sort and uses one set per level instead.

Permutations (LC 47) have no startIndex, so the rule is written i > 0 && nums[i] == nums[i-1] && !used[i-1]. The value of used[i-1] tells you which of the two situations you are in:

Value of used[i-1]What it means in the treeSkip?Why
!used[i-1]
(the equal element was already undone)
Same level. The first 1 was tried at this node and undone; now the second 1 is up.SkipTwo equal values at one node build identical subtrees, so the second one only repeats answers.
used[i-1]
(the equal element is still on the path)
Along the path. The first 1 is an ancestor; this level is appending the second 1 after it.KeepThis is the real permutation [1,1]; removing it loses answers.

Both rules chase the same goal

Do not let the two names confuse you. Both express one rule: never pick two equal values at the same node. Combinations and subsets have a startIndex, so i > start already means "same node". Permutations have no startIndex, so !used[i-1] is used to ask "was the equal value just tried and undone at this node?". Using used[i-1] instead also produces correct answers — it forces duplicates to be taken in the opposite index order — but it discovers the repetition deeper in the tree, so it prunes later and runs slower. The standard form is !used[i-1].

§09

Featured problem D · LC 51 N-Queens: the decision tree on a board

HARD

Rows become the levels of the tree and columns become the choices at each level.

Problem: place n queens on an n×n board so that no two share a row, a column, or a diagonal, and return every arrangement. Building the tree: a valid board has exactly one queen per row, otherwise two would share a row. So the row number is the level of the tree and the column is the choice at that level. dfs(row) tries every column in row row; if the square is safe it recurses into row+1; reaching row n means all rows are filled and one solution is complete.

The work is in the safety check: before placing at (row, col) you must know that no queen above attacks it — same column, same "\" diagonal, or same "/" diagonal. Place a 4×4 board yourself below; watch the red conflicts and the dead end that forces a step back:

LC 51 N-Queens · placing row by row on a 4×4 board (♛ = queen · red = conflict · outlined = the queen doing the attacking)
An 4×4 board. No two queens may share a row, a column, or a diagonal. Queens go down one row at a time, and each row picks a square that no queen above it attacks.
1 / 36

Those moments where a whole row has no safe square, so the queen in the row above has to move, are backtracking on a board. The safety check can be made O(1) with three sets that record which columns and which diagonals already hold a queen. Why do row − col and row + col identify a diagonal? Along a "\" diagonal, moving one square down-right adds 1 to both row and col, so row − col stays the same. Along a "/" diagonal, moving one square down-left adds 1 to row and subtracts 1 from col, so row + col stays the same. Each value therefore names exactly one diagonal.

lc51_n_queens.py
1class Solution:
2 def solveNQueens(self, n: int) -> list[list[str]]:
3 res = []
4 queens = [-1] * n
5 col, diag1, diag2 = set(), set(), set() # columns / r-c / r+c
6
7 def backtrack(r: int) -> None:
8 if r == n:
9 res.append(["." * c + "Q" + "." * (n - c - 1) for c in queens])
10 return
11 for c in range(n):
12 if c in col or (r - c) in diag1 or (r + c) in diag2:
13 continue # attacked: skip
14 queens[r] = c
15 col.add(c); diag1.add(r - c); diag2.add(r + c) # 1. choose
16 backtrack(r + 1) # 2. recurse
17 col.discard(c); diag1.discard(r - c); diag2.discard(r + c) # 3. undo
18
19 backtrack(0)
20 return res
Python stores the diagonals in a set, so a negative key is fine and no + n shift is needed. The list comprehension builds each row as "." * c + "Q" + "." * (n-c-1).

Eight queens: a 170-year-old puzzle

The eight queens puzzle was posed in 1848 by the chess player Max Bezzel. Gauss worked on it and at one point miscounted the solutions. In 1972 Edsger Dijkstra used it as a teaching example for structured programming and backtracking. An 8×8 board has exactly 92 solutions.

Sudoku (LC 37) is the two-dimensional version of the same idea: find an empty cell, try the digits 1 to 9, place a digit that breaks no rule and recurse on the rest of the board, and erase it if the recursion fails. The row, column, and 3×3 box checks are its constraint pruning. A faster version packs the digits already used in each row, column, and box into the bits of one integer, which turns both the check and the "which digits are still allowed" question into bit operations. See chapter 04 · bit manipulation for representing a set with one integer and using lowbit to read candidates.

Backtracking runs inside software you use every day

(1) Regular expression engines. Most languages match by backtracking. A pattern such as (a+)+$ fed a string that almost matches can make the engine explore an exponential number of ways to split the input. The CPU saturates, and the resulting denial-of-service is called catastrophic backtracking, or ReDoS. (2) SAT and constraint solvers. The DPLL algorithm behind scheduling, sudoku, and hardware verification is backtracking with strong pruning. (3) Prolog. Its entire execution model is backtracking. Understanding this chapter tells you why these systems are fast most of the time and suddenly very slow on some inputs.

Complexity and follow-up questions

Count nodes times work per node. The column rule alone leaves at most n choices in row 0, n−1 in row 1, and so on, so the search examines at most n! complete placements, where n is the board size. Each solution is then written out as n strings of length n, which costs O(n²). The diagonal checks remove the large majority of those branches in practice, and the real number of solutions grows much more slowly than n!, but no better worst-case bound is known — pruning changes the running time, not the bound. Space is O(n) for the recursion and the three sets.

Follow-ups: (1) "only the number of solutions?" → LC 52, skip building the board and just increment a counter; (2) "faster for larger n?" → replace the three boolean arrays with bitmasks, one integer per constraint, and read the available columns with lowbit (chapter 04); (3) "does sudoku work the same way?" → yes, LC 37 uses the same structure plus the bit optimisation.

§10

Problem set: 15 backtracking problems

Core plus advanced

Grouped as combinations, partitioning, subsets, permutations, and boards, from easier to harder. Think for 30 seconds before opening a hint.

§11

Quiz

✎ Quiz

Get all 8 right to mark this chapter complete.

QUESTION 01 / 8

Which sentence describes backtracking most accurately?

QUESTION 02 / 8

LC 77 Combinations: choose k numbers from 1..n. Why does the recursion carry a startIndex and only pick from it forward?

QUESTION 03 / 8

LC 77 with n = 4 and k = 2: how many combinations are there? (Count the leaves in the decision tree, or compute C(4,2).)

QUESTION 04 / 8

Combinations, subsets, and permutations all run on the same kind of decision tree. When does each of them record an answer?

QUESTION 05 / 8

Why does LC 46 Permutations use a used boolean array instead of a startIndex like combinations?

QUESTION 06 / 8

In LC 47 Permutations II (the input has duplicates) the skip condition is i>0 && nums[i]==nums[i-1] && !used[i-1]. What is !used[i-1] there for?

QUESTION 07 / 8

Which statements about pruning are correct? (Select all that apply.)

QUESTION 08 / 8

The template undoes the choice after every recursive call returns (path.pop(), used[i]=false, erase the square). What happens if you forget the undo?

What to take away from this chapter
  • Backtracking is a depth-first search over a tree of partial answers: choose, recurse, un-choose. The un-choose is what makes it backtracking; without it the shared path is polluted and the answers are wrong.
  • Answer the three questions first: the path (what is chosen), the choices available now, and the stop condition. The template then writes itself.
  • Recording a solution must copy the path: Java new ArrayList<>(path), Python path[:], JS [...path]. Storing the path itself stores a reference that later undo steps will change.
  • What a deeper call may reuse is the organising idea. Subsets, combinations, and partitions move a startIndex forward; permutations track a used set. Subsets record at every node; combinations and permutations record at the leaves.
  • Pruning comes in two kinds: a branch that cannot be valid (a constraint check, used everywhere in this chapter) and a branch that cannot be better (a bound, used when searching for an optimum). LC 77's upper limit of n−(k−chosen)+1 is the model case.
  • For duplicate values, sort first and skip equal values at the same node: i>start && nums[i]==nums[i-1] for combinations and subsets, !used[i-1] for permutations. Both express one rule.
  • For boards (51 / 37), rows are levels and columns are choices. A column set plus the two diagonal sets, indexed by row−col and row+col, make the safety check O(1).