Skip to content

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

Backtracking with pruning

Explanation

Pruning means cutting off a branch of the decision tree as soon as you know it cannot lead to a valid answer, instead of building it to the end and then rejecting it. A simple check at each step (the sum is already too big, there are more closing brackets than opening ones, two queens attack each other) can make backtracking thousands of times faster. Combination sum, phone letter combinations, balanced parentheses, N-Queens and Sudoku are the classic examples.

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.

Think of it like this: Looking for a flat to rent within ₹20,000 a month, you do not visit every flat in the city and check the rent at the end. As soon as an area is clearly above budget, you skip all the flats in it. That is pruning.

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.

python
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.

python
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: ['((()))', '(()())', '(())()', '()(())', '()()()']
Trying "(" before ")" produces the strings in sorted order, because "(" comes before ")" in character order.

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.

python
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 92

Sudoku: 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

Pruning

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.

python
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)
Shows True {'Maths': 0, 'Physics': 1, 'Chemistry': 2, 'English': 0}.

Practice (0/4 done)

Task 1 · Code · Python

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"].

Task 2 · Code · Python

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 ["(())", "()()"].

Task 3 · Code · Python

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]].

Task 4 · Code · Python

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

Question 1

1.What does pruning do in backtracking?

Question 2

2.In combination sum with reuse allowed, which index goes into the recursive call?

Question 3

3.When generating balanced parentheses, when may you add ")"?

Question 4

4.In N-Queens, which value is the same for every cell on one down-right diagonal?

Question 5

5.Why must candidates be sorted before using break when c > remaining?

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.

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.

From Backtracking: subsets and permutations

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.

From Recursive tree patterns

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.

From Self joins and cross joins

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. 1 more topic in “Recursion and backtracking” before its exam: 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: Backtracking: subsets and permutations