Backtracking explores choices step by step and returns when a partial choice cannot lead to a valid answer.
It is a structured way to search through combinations, permutations, constraints, and decision trees.
Core Idea
Backtracking builds a partial solution. At each step, it tries a choice, recurses, and then undoes the choice before trying the next one.
The important optimization is pruning: if a partial solution already violates a rule, the algorithm stops exploring that branch.
The backtracking shape is choose, explore, unchoose. That shape lets one shared current list represent the partial solution while recursion moves through the decision tree.
Function Contracts
subsets(values) accepts a sequence and returns a list of all subsets represented as lists. The number of returned subsets is 2 ** len(values).
permutations(values) accepts a sequence and returns every ordering of the input values. The number of returned permutations is len(values)!, so this is only practical for small inputs.
Python Example
def subsets(values):
result = []
current = []
def search(index):
if index == len(values):
result.append(current.copy())
return
search(index + 1)
current.append(values[index])
search(index + 1)
current.pop()
search(0)
return resultThe append chooses an item. The pop undoes that choice.
The Choose-Explore-Unchoose Shape
Many backtracking solutions follow the same control flow: choose one option, explore everything under that option, then undo the option before trying the next one.
Permutations show the shape with concrete code:
def permutations(values):
result = []
current = []
used = [False] * len(values)
def search():
if len(current) == len(values):
result.append(current.copy())
return
for i, value in enumerate(values):
if used[i]:
continue
used[i] = True
current.append(value)
search()
current.pop()
used[i] = False
search()
return resultcurrent.append(value) chooses. The recursive call explores. current.pop() and used[i] = False undo the choice so the next branch starts from a clean state.
Pruning
Pruning means refusing to explore a branch once it cannot succeed.
For example, in a combination-sum problem, if all numbers are positive and the current sum is already greater than the target, adding more numbers cannot fix it. That branch can stop immediately.
Pruning does not change the set of valid answers. It only avoids work that cannot produce one.
Complexity
The cost depends on how many branches are actually explored. Generating all subsets of n values takes O(2^n) time because there are 2^n subsets to output.
Generating all permutations takes O(n!) branches, and each recorded permutation has length n, so the output itself is large.
Pruning can make a practical problem much faster, but the worst-case search space may still be exponential.
Failure Example
If current.pop() is forgotten in the subset example, the choice made in one branch leaks into the next branch. The result then contains combinations that do not match the intended choice path.
Backtracking code is correct only when each recursive call leaves shared mutable state exactly as it found it before returning.
Common Confusions
Backtracking is not just recursion. It is recursion plus controlled choice, constraint checking, and undoing state.
Forgetting to undo a choice is a common bug. Shared mutable state must be restored before the function returns to the previous level.
Another common bug is appending current directly to the results. Use current.copy() when recording a list, because current will continue changing after recursion returns.
When To Use It
Use backtracking for permutations, subsets, combinations, constraint satisfaction, board problems, and any problem where choices form a tree of possibilities.
Backtracking is usually appropriate when the output itself may be large or when constraints can prune most branches.
The Main Point
Backtracking explores a choice tree while carefully undoing each choice. It is useful when constraints can prune branches before the full search space is explored.