Pruning is the win
A backtracking search is only fast because it abandons hopeless branches early. The test that prunes is the design.
Backtracking tries a choice, recurses, and undoes it if the choice cannot lead to a solution. It is recursion that explores options and walks back.
About 3 min of reading
Explore options with backtracking.
All rows of length 2.
Before you run it, say out loud what it will print. Then open the answer.
Subsets of 1,2.
Before you run it, say out loud what it will print. Then open the answer.
Count ways to 4.
Before you run it, say out loud what it will print. Then open the answer.
Permutations of 1,2,3.
Before you run it, say out loud what it will print. Then open the answer.
Before you write anything, put these lines back in the right order. Click a line to move it up one step. This support step is easier than writing code from scratch, and it is here to make the last example stick.
Read the first example one line at a time. A line that prints shows what appears on screen; the rest set things up quietly.
A backtracking search is only fast because it abandons hopeless branches early. The test that prunes is the design.
Building new partial answers avoids undo bugs. Mutating shared state is faster but you must undo exactly.
def pick(n, chosen):
if n == 0:
return [chosen]
out = []
for c in "ab":
out += pick(n - 1, chosen + c)
return out
print(pick(2, ""))
Each choice extends the row, then the call explores deeper. All rows come back.
def solve(n):
if n < 0:
return 0
if n == 0:
return 1
return solve(n - 1) + solve(n - 2)
print(solve(4))
A negative n returns zero, pruning that branch. The base cases end the search.
def subsets(a):
if not a:
return [[]]
rest = subsets(a[1:])
return rest + [[a[0]] + s for s in rest]
print(subsets([1, 2]))
Every subset either holds the head or not. Two recursive groups build them all.
def count(n, cap):
if n > cap:
return 0
if n == cap:
return 1
return count(n + 1, cap) + count(n + 2, cap)
print(count(0, 4))
Going over the cap returns zero right away. Pruning cuts whole branches.
def perms(a):
if not a:
return [[]]
out = []
for i in range(len(a)):
rest = a[:i] + a[i + 1:]
for p in perms(rest):
out.append([a[i]] + p)
return out
print(perms([1, 2, 3]))
Fix one item, permute the rest. Recursion builds every order.
def subsets(a):
if not a:
return [[]]
rest = subsets(a[1:])
return rest + [[a[0]] + s for s in rest]
print(len(subsets([1, 2, 3])))
Building new lists means no shared state to undo. The undo is the copy.
def pick(n, chosen):
if n == 0:
return [chosen]
out = []
for c in "ab":
out += pick(n - 1, chosen + c)
return out
print(len(pick(3, "")))
Three choices of two give eight rows. The tree of calls is the search space.
def solve(n):
if n < 0:
return 0
if n == 0:
return 1
return solve(n - 1) + solve(n - 2)
print(solve(0))
The exact target returns one solution. Below it returns zero, ending dead branches.
Each one is defined here on this site. Open the entry for the full definition and the other words it points at.