Backtracking Template: Choose, Explore, Unchoose
Implement the three-step backtracking skeleton, trace it on a small example, and identify where pruning conditions slot in.
Backtracking Template: Choose, Explore, Unchoose is a free DSA Interview Prep lesson on CoddyKit — lesson 1 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the DSA Interview Prep learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
What is Backtracking?
Backtracking is a systematic method for finding all (or some) solutions by exploring every candidate incrementally and abandoning (pruning) a branch as soon as it is determined that the branch cannot yield a valid solution. It is the algorithm behind solving Sudoku, generating permutations, and finding all valid combinations. Think of it as a depth-first search on a decision tree.
# Mental model: backtracking explores a decision tree
# At each node you make a choice, go deeper, then undo it
#
# Tree for generating subsets of [1,2,3]:
# []
# / \
# [1] []
# / \ / \
# [1,2][1][2] []
# ...
# Every leaf is a potential solution
# Pruning cuts branches early based on constraints
print('Backtracking = DFS on decision tree with pruning')The Three-Step Template
Every backtracking function follows three steps: Choose — pick the next candidate from available options. Explore — recurse with that choice, moving one level deeper in the decision tree. Unchoose — undo the choice after returning from recursion to restore state for the next candidate. This pattern is also called add/recurse/remove or mark/recurse/unmark in different contexts.
def backtrack(current_state, choices, results):
# Base case: is current_state a complete solution?
if is_complete(current_state):
results.append(list(current_state)) # record solution
return
for choice in choices:
if is_valid(choice, current_state): # pruning condition
# 1. CHOOSE
current_state.append(choice)
# 2. EXPLORE
backtrack(current_state, choices, results)
# 3. UNCHOOSE (backtrack)
current_state.pop()
# Placeholder functions — filled per problem
def is_complete(state): return True
def is_valid(choice, state): return TrueSimplest Example: All Subsets
Generate all subsets of [1, 2, 3]. At each index, we choose to include or exclude the element. The start index advances after each call so we don't revisit previous elements. No constraint check is needed — every partial state is valid. This produces 2ⁿ subsets. The unchoose step is path.pop() after the recursive call.
def subsets(nums):
result = []
def backtrack(start, path):
result.append(list(path)) # every state is a valid subset
for i in range(start, len(nums)):
path.append(nums[i]) # CHOOSE
backtrack(i + 1, path) # EXPLORE
path.pop() # UNCHOOSE
backtrack(0, [])
return result
print(subsets([1, 2, 3]))
# [[], [1], [1,2], [1,2,3], [1,3], [2], [2,3], [3]]Identifying the Pruning Condition
The power of backtracking over brute force lies in pruning: recognising early that a partial path cannot lead to a valid solution. For combination sum (target sum with a budget), once the running sum exceeds the target, any deeper branch will only grow larger — prune by returning immediately. For N-queens, if a queen attacks existing queens, skip that column. Pruning turns exponential trees into manageable searches.
def combination_sum(candidates, target):
result = []
candidates.sort() # sort enables early termination
def backtrack(start, path, remaining):
if remaining == 0:
result.append(list(path))
return
for i in range(start, len(candidates)):
c = candidates[i]
if c > remaining: break # PRUNE: sorted, so rest are bigger too
path.append(c) # CHOOSE
backtrack(i, path, remaining - c) # EXPLORE (reuse allowed)
path.pop() # UNCHOOSE
backtrack(0, [], target)
return result
print(combination_sum([2, 3, 6, 7], 7)) # [[2,2,3],[7]]State Restoration is Critical
A common bug in backtracking is failing to fully restore state before the next iteration. If you use a mutable data structure (list, set, grid), every modification made during Choose must be reversed in Unchoose. For example, when modifying a grid (like Sudoku or Word Search), set the cell to empty after the recursive call. Forgetting this leaves the state corrupted for sibling branches.
# Bug: forgetting to unmark in word search
# Correct pattern for grid backtracking:
def word_search(board, word):
m, n = len(board), len(board[0])
def dfs(r, c, k):
if k == len(word): return True
if not (0<=r<m and 0<=c<n): return False
if board[r][c] != word[k]: return False
temp, board[r][c] = board[r][c], '#' # CHOOSE (mark visited)
found = any(dfs(r+dr, c+dc, k+1)
for dr, dc in [(0,1),(0,-1),(1,0),(-1,0)])
board[r][c] = temp # UNCHOOSE (restore cell)
return found
return any(dfs(r, c, 0) for r in range(m) for c in range(n))
board = [['A','B','C','E'],['S','F','C','S'],['A','D','E','E']]
print(word_search([row[:] for row in board], 'ABCCED')) # TrueTracing the Decision Tree
For combination sum with [2, 3, 6, 7] and target 7, trace the tree: at the root, try 2. From 2, try 2 again (remaining=3). From 2+2, try 2 again (remaining=1). 2>1 so prune. Try 3: 3>1 prune. Backtrack. From 2+2: try 3 (remaining=3). 3 matches remaining: record [2,2,3]. Backtrack and continue. This trace shows how pruning eliminates branches before they produce invalid results.
def combination_sum_trace(candidates, target):
result = []
candidates.sort()
def backtrack(start, path, remaining, depth):
indent = ' ' * depth
print(f'{indent}explore({path}, remaining={remaining})')
if remaining == 0:
result.append(list(path))
print(f'{indent}FOUND: {path}')
return
for i in range(start, len(candidates)):
c = candidates[i]
if c > remaining:
print(f'{indent}PRUNE at {c}')
break
path.append(c)
backtrack(i, path, remaining - c, depth + 1)
path.pop()
backtrack(0, [], target, 0)
return result
combination_sum_trace([2, 3, 6, 7], 7)Backtracking vs Brute Force
Brute force tries all possible complete solutions and then validates each. Backtracking prunes during construction, never completing invalid paths. For N-queens with N=8, brute force checks 8^8 = 16 million placements. Backtracking reduces this to around 2,057 recursive calls. The difference grows dramatically with N: N=12 brute force tries 8.9 billion placements while backtracking explores only a fraction of the tree.
# Compare call counts: brute force vs backtracking for permutations
import sys
calls_brute = [0]
calls_back = [0]
def brute_force_perms(nums):
from itertools import permutations
return list(permutations(nums))
def backtrack_perms(nums):
result = []
used = [False] * len(nums)
def bt(path):
calls_back[0] += 1
if len(path) == len(nums):
result.append(list(path))
return
for i, n in enumerate(nums):
if not used[i]:
used[i] = True
path.append(n)
bt(path)
path.pop()
used[i] = False
bt([])
return result
backtrack_perms([1,2,3,4])
print(f'Backtrack calls for 4 items: {calls_back[0]}')Collecting vs Returning Early
Backtracking problems fall into two categories: enumerate all solutions (collect every complete path) or find any one solution (return True as soon as a path succeeds). For enumeration, always append to a results list. For find-any, return True immediately from the recursive call and propagate it up. Returning any(backtrack(...)) or if backtrack(...): return True implements the short-circuit behaviour.
# Enumerate all: collect in results list
def all_solutions(candidates):
results = []
def bt(path, remaining):
if remaining == 0:
results.append(list(path))
return
for c in candidates:
if c <= remaining:
path.append(c); bt(path, remaining - c); path.pop()
bt([], 5)
return results
# Find any one: return True on first success
def any_solution(candidates, target):
def bt(path, remaining):
if remaining == 0: return True
for c in candidates:
if c <= remaining:
path.append(c)
if bt(path, remaining - c): return True # short-circuit
path.pop()
return False
path = []
return bt(path, target), pathMemoisation with Backtracking
Pure backtracking explores every path without caching, which is fine when all solutions are needed. However, some backtracking problems have overlapping sub-problems. For example, Word Break II can be solved with backtracking + memoisation: cache the list of sentences possible from each starting index. This converts the worst-case exponential backtracking into a polynomial-time algorithm. Recognise when sub-problems repeat to apply this hybrid.
from functools import lru_cache
def word_break_all(s, wordDict):
words = set(wordDict)
@lru_cache(maxsize=None)
def bt(start):
if start == len(s): return [''] # empty suffix
result = []
for end in range(start + 1, len(s) + 1):
word = s[start:end]
if word in words:
for rest in bt(end):
result.append(word if not rest else word + ' ' + rest)
return result
return bt(0)
print(word_break_all('catsanddog', ['cat','cats','and','sand','dog']))
# ['cat sand dog', 'cats and dog']Time Complexity of Backtracking
Backtracking time complexity depends on the number of leaves in the decision tree times the work per node. For subsets: O(n × 2ⁿ). For permutations: O(n × n!). For combination sum: O(target/min_candidate ^ n) in the worst case. Pruning reduces the constant but not the asymptotic bound. When asked for complexity in an interview, give the worst-case tree size and mention that pruning typically makes it much faster in practice.
# Complexity quick reference:
# Subsets of n elements: O(n * 2^n) - 2^n subsets, each copied in O(n)
# Permutations of n: O(n * n!) - n! perms, each copied in O(n)
# Combination sum (target T): O(T^n / n!) worst case without pruning
# N-Queens: O(n!) - prune reduces practical count
# For n=10 permutations: 10! = 3,628,800 paths
import math
n = 10
print(f'n={n}: n!={math.factorial(n):,} paths')
print(f'n={n}: 2^n={2**n:,} subsets')Identifying Backtracking Problems
Signals that a problem needs backtracking: (1) Find all or generate all combinations, permutations, or subsets. (2) The problem involves placing items or people under constraints (N-queens, Sudoku). (3) The solution space is exponential but constraints eliminate most branches early. (4) You need to explore paths in a graph or grid that might revisit states. When you see these signals, reach for the choose-explore-unchoose template.
# Common backtracking problem types:
# 1. Subsets / Power set
# 2. Permutations (with/without duplicates)
# 3. Combinations (k from n, combination sum)
# 4. Grid path finding (word search, unique paths with visited tracking)
# 5. Constraint satisfaction (N-queens, Sudoku solver)
# 6. String partitioning (palindrome partition, word break all)
# Template reminder:
def backtrack(start, path):
# base case: add to results or return True
for choice in get_choices(start):
if is_valid(choice, path): # prune
path.append(choice) # choose
backtrack(start+1, path) # explore
path.pop() # unchoose
def get_choices(start): return []
def is_valid(c, p): return TrueQuick Check
Test your understanding of Data Structures & Algorithms — Coding Interview Prep concepts from this lesson.
Lesson Recap
In this lesson you learned: the backtracking template has three steps — choose, explore, unchoose — which correspond to adding a choice, recursing, and removing it, pruning conditions eliminate branches early and are what make backtracking practical vs brute force, and state must be fully restored after each recursive call to avoid corrupting sibling branches. Next up we apply the template to generate all Subsets and the Power Set.
Frequently asked questions
Is the “Backtracking Template: Choose, Explore, Unchoose” lesson free?
Yes — the full text of “Backtracking Template: Choose, Explore, Unchoose” is free to read here on the web, and the DSA Interview Prep course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the DSA Interview Prep course, upgrade to CoddyKit PRO.
What will I learn in “Backtracking Template: Choose, Explore, Unchoose”?
Implement the three-step backtracking skeleton, trace it on a small example, and identify where pruning conditions slot in. You practise DSA Interview Prep with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start DSA Interview Prep?
No prior experience is required. DSA Interview Prep on CoddyKit is structured for beginners through advanced learners; this is — lesson 1 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “Backtracking Template: Choose, Explore, Unchoose” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this DSA Interview Prep lesson?
Yes. Every DSA Interview Prep lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- Backtracking Template: Choose, Explore, Unchoose
- Subsets and Power Set
- Permutations and Combinations
- N-Queens and Constraint Propagation