Intermediate: core algorithms · Recursion and backtracking · topic 24 of 61 · 40 min
Backtracking with pruning
Explanation
Why prune?
Plain backtracking builds every path to the end. Often you can tell much earlier that a partial path is hopeless: the running total already passed the target, or two queens already attack each other. Pruning is the act of returning immediately from such a call. The answers do not change; you just stop exploring branches that cannot produce any. The worst case Big-O may look the same, but in practice the number of calls can drop from millions to thousands.
Combination sum: reuse allowed
Given distinct positive candidates and a target, list every combination that adds up to the target, where each candidate can be used any number of times. To avoid listing [2, 3, 2] and [2, 2, 3] separately, build each combination in non-decreasing order: pass the current index down, not i + 1, because the same candidate may be used again. Sort the candidates so you can prune: once a candidate is bigger than what remains, every later one is too, so break out of the loop.
def combination_sum(candidates, target):
candidates = sorted(candidates)
result, path = [], []
def backtrack(start, remaining):
if remaining == 0:
result.append(path[:])
return
for i in range(start, len(candidates)):
c = candidates[i]
if c > remaining: # prune: this and all later ones are too big
break
path.append(c)
backtrack(i, remaining - c) # i, not i + 1: c may be reused
path.pop()
backtrack(0, target)
return result
print(combination_sum([2, 3, 6, 7], 7)) # Output: [[2, 2, 3], [7]]Letter combinations of a phone number
On an old phone keypad, 2 is "abc", 3 is "def", 4 "ghi", 5 "jkl", 6 "mno", 7 "pqrs", 8 "tuv" and 9 "wxyz". Given digits such as "23", list every word the keys could type: ad, ae, af, bd, … , cf. Each level of the tree is one digit and its branches are that digit's letters, so there are up to 4ⁿ results for n digits. There is nothing to prune, but it is the same choose-explore-un-choose shape. Watch the edge case: empty input gives an empty list, not [""].
Generating balanced parentheses
List every string of n pairs of brackets that is balanced, like "(())" and "()()" for n = 2. Instead of generating all 2²ⁿ strings and checking each, prune while building: you may add "(" only while fewer than n have been used, and ")" only while there are more open brackets than closed ones. Every completed string is then valid by construction. The number of answers is the nth Catalan number: 1, 1, 2, 5, 14, 42, … for n = 0, 1, 2, 3, 4, 5.
def generate_parentheses(n):
result = []
def backtrack(path, opened, closed):
if len(path) == 2 * n:
result.append("".join(path))
return
if opened < n:
path.append("("); backtrack(path, opened + 1, closed); path.pop()
if closed < opened:
path.append(")"); backtrack(path, opened, closed + 1); path.pop()
backtrack([], 0, 0)
return result
print(generate_parentheses(3))
# Output: ['((()))', '(()())', '(())()', '()(())', '()()()']N-Queens
Place n chess queens on an n × n board so that no two share a row, a column or a diagonal. Put exactly one queen in each row, going down row by row; at each row, try each column. Pruning makes it fast: keep three sets: used columns, used "r − c" diagonals (cells on one down-right diagonal share r − c) and used "r + c" anti-diagonals. A column is safe only if all three checks pass, so most branches end within a few rows. For n = 8 there are 92 solutions; for n = 2 and n = 3 there are none.
def n_queens_count(n):
cols, diag, anti = set(), set(), set()
def place(r):
if r == n:
return 1
total = 0
for c in range(n):
if c in cols or (r - c) in diag or (r + c) in anti:
continue # prune: attacked
cols.add(c); diag.add(r - c); anti.add(r + c)
total += place(r + 1)
cols.remove(c); diag.remove(r - c); anti.remove(r + c)
return total
return place(0)
print(n_queens_count(4), n_queens_count(8)) # Output: 2 92Sudoku: the classic example
A Sudoku solver uses the same plan. Find an empty cell, try digits 1 to 9, and keep a digit only if it does not already appear in that cell's row, column or 3 × 3 box (sets make each check O(1)). Recurse to the next empty cell; if no digit fits somewhere later, undo and try the next digit. Good solvers prune harder by filling first the cell with the fewest possible digits. You do not need to code it here, but you should be able to describe it in an interview.
Common mistakes
- Pruning that drops valid answers, for example break when the candidates are not sorted. Fix: sort first, or use continue instead of break.
- Using i + 1 in combination sum when reuse is allowed (or i when it is not). Fix: decide whether an item can repeat, and pass i or i + 1 to match.
- Forgetting to remove a queen's column and diagonals after the recursive call. Fix: undo exactly what you added, in the same call.
- Returning [""] for empty digits in letter combinations. Fix: handle the empty input first and return [].
- Checking validity only at the end, which wastes time on dead branches. Fix: check as early as possible, before recursing.
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 with pruning · 1/8
This lesson adds pruning to backtracking: stop exploring a branch as soon as it cannot lead to a valid answer.
Prefer a video by a teacher? Find free videos on “backtracking pruning combination sum n queens generate parentheses explained” on YouTube ↗
In real life: Building an exam timetable
A university must place exams into time slots so that no student has two exams at once. A simple scheduler places one exam at a time, tries each free slot, and backtracks when an exam has no slot left. Pruning is essential: as soon as a slot clashes with an exam already placed for the same students, that branch is cut. Real timetabling tools add heuristics such as placing the most constrained exam first, the same idea as filling the most constrained Sudoku cell first.
exams = ["Maths", "Physics", "Chemistry", "English"]
clash = {("Maths", "Physics"), ("Physics", "Chemistry"), ("Maths", "Chemistry")}
slots = 3
plan = {}
def ok(exam, slot):
return all(not ((exam, e) in clash or (e, exam) in clash) or s != slot
for e, s in plan.items())
def place(i):
if i == len(exams):
return True
for slot in range(slots):
if ok(exams[i], slot): # prune clashing slots early
plan[exams[i]] = slot
if place(i + 1):
return True
del plan[exams[i]] # backtrack
return False
print(place(0), plan)Practice (0/4 done)
Write letter_combinations(digits) for a string of digits from 2 to 9. Using the phone keypad (2 abc, 3 def, 4 ghi, 5 jkl, 6 mno, 7 pqrs, 8 tuv, 9 wxyz), return every string the digits could type, in sorted (alphabetical) order. An empty string gives [] (an empty list). Example: letter_combinations("23") returns ["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"].
Write generate_parentheses(n) that returns every balanced string of n pairs of round brackets, in sorted order (Python's normal string order, where "(" comes before ")"). Prune while building: add "(" only if fewer than n are used, and ")" only if fewer closing than opening brackets are used. n = 0 gives [""] (one empty string). Example: generate_parentheses(2) returns ["(())", "()()"].
Write combination_sum(candidates, target). candidates are DISTINCT positive integers (not necessarily sorted) and target ≥ 1. Return every different combination of candidates that adds up to target, where each candidate may be used ANY number of times. Each combination is a list; [2, 2, 3] and [3, 2, 2] count as the same combination, so return it once. The tests sort before comparing, so any order is fine. Sort the candidates and prune with break when a candidate is bigger than what remains. Example: combination_sum([2, 3, 6, 7], 7) returns [[2, 2, 3], [7]].
Write n_queens_count(n) that returns how many ways there are to place n queens on an n × n chessboard so that no two attack each other (no shared row, column or diagonal). Place one queen per row and prune with three sets: used columns, used r − c diagonals and used r + c anti-diagonals. Known answers: n = 1 → 1, n = 4 → 2, n = 8 → 92. Assume 1 ≤ n ≤ 9; it must finish in well under a second for n = 9.
Quick quiz
Read more (free)
Related help
Before this, learn
This topic builds on these. If something here feels new, open them first.
Related topics
- Memoization: remembering answersRecursion and backtracking · same module
- Divide and conquerRecursion and backtracking · same module
- Recursive tree patternsTrees and binary search trees
- 1-D DP classicsDynamic programming
- Self joins and cross joinsSQL and Databases · JOINs
- Data-structure questions in JavaScriptJavaScript Programming (language in depth) · Interview preparation and career
Questions people ask about this
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.
What does pruning do in backtracking?
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.
In combination sum with reuse allowed, which index goes into the recursive call?
Answer: i, so the same candidate can be chosen again
Passing i allows reuse while keeping each combination in non-decreasing order, so no duplicates appear.
When generating balanced parentheses, when may you add ")"?
Answer: Only when fewer ")" than "(" have been used so far
If closed < opened, there is an open bracket to close; otherwise the prefix would already be unbalanced.
In N-Queens, which value is the same for every cell on one down-right diagonal?
Answer: r − c
Moving down-right adds 1 to both r and c, so r − c stays the same. Anti-diagonals share r + c.
Why must candidates be sorted before using break when c > remaining?
Answer: Otherwise a smaller candidate later in the list would be skipped wrongly
break assumes every later candidate is at least as big; that is true only after sorting.
Why does backtracking save path[:] instead of path?Related
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.
For has_path_sum, the tree is build([1, 2]) and the target is 1. What is the right answer?Related
Answer: False, because the root is not a leaf
The path must end at a leaf. The only root-to-leaf path is 1 → 2, which sums to 3.
In the ways-to-make-change DP, putting the AMOUNTS in the outer loop and coins in the inner loop counts…Related
Answer: Ordered sequences, so 1+2 and 2+1 are counted separately
With amounts outside, every coin can be the last one at every amount, so each ordering is a separate path.
From 1-D DP classics
The query "employees who earn more than their manager" returns no rows on COMPANY. What should you do first?Related
Answer: Look at the data: if every manager earns more, an empty result is correct
An empty result can be the right answer. Check before changing the query.
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 more topic in “Recursion and backtracking” before its exam: Memoization: remembering answers.
- Module 5 exam: Recursion and backtracking · 14 questions · 25 min, without help. 5 topics of this module still to finish first.
- Then the next module, Sorting and searching. 61 topics left in the course before the final exam.