mirror of
https://github.com/prdlk/leetcode.git
synced 2026-11-02 21:22:43 +00:00
163 lines
4.7 KiB
Plaintext
163 lines
4.7 KiB
Plaintext
---
|
||||
|
|
title: Backtracking
|
|||
|
|
description: 'Backtracking is a depth-first search technique that builds a solution step by step and abandons (undoes) a partial choice as soon as it can’t lead to a valid complete solution.'
|
|||
|
|
hidden: true
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## What it is
|
|||
|
|
|
|||
|
|
Backtracking explores possible paths to a solution one step at a time. When a path reaches a dead end, you undo the last step and try a different option. Think of every problem as a decision tree. The root is an empty answer. Each level is a decision. Each branch is one option.
|
|||
|
|
|
|||
|
|
## When to use it
|
|||
|
|
|
|||
|
|
Use backtracking when you must generate **all** valid solutions or evaluate **all** combinations. Do not use it when you need only one solution or the optimal solution. For those, use greedy or dynamic programming. Backtracking has exponential time complexity.
|
|||
|
|
|
|||
|
|
## The four components
|
|||
|
|
|
|||
|
|
Every backtracking problem has the same parts:
|
|||
|
|
|
|||
|
|
1. **Base case** — when to save the current path and stop.
|
|||
|
|
2. **Choices** — the options at each step.
|
|||
|
|
3. **Constraints** — the rules that reject an invalid path early.
|
|||
|
|
4. **Backtrack step** — undo the last choice and try the next one.
|
|||
|
|
|
|||
|
|
## The template
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
def solve(data):
|
|||
|
|
result = []
|
|||
|
|
path = []
|
|||
|
|
|
|||
|
|
def backtrack(index):
|
|||
|
|
# 1. Base case
|
|||
|
|
if is_complete(index, path):
|
|||
|
|
result.append(path[:]) # copy — lists are passed by reference
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
# 2. Choices
|
|||
|
|
for choice in get_choices(index, data):
|
|||
|
|
# 3. Constraints
|
|||
|
|
if not is_valid(choice, path):
|
|||
|
|
continue
|
|||
|
|
|
|||
|
|
path.append(choice) # make the choice
|
|||
|
|
backtrack(index + 1) # explore
|
|||
|
|
path.pop() # 4. Backtrack
|
|||
|
|
|
|||
|
|
backtrack(0)
|
|||
|
|
return result
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Steps to apply the template
|
|||
|
|
|
|||
|
|
1. Draw the decision tree for a small input. Name each level and each branch.
|
|||
|
|
2. Write the base case. Ask: when is the path complete?
|
|||
|
|
3. Write the choices. Ask: what can I add at this level?
|
|||
|
|
4. Write the constraints. Ask: what makes a path invalid? Reject it before recursion.
|
|||
|
|
5. Add the backtrack step. Pop what you appended.
|
|||
|
|
6. Copy the path when you save it. Never append the same list reference.
|
|||
|
|
|
|||
|
|
## Four problems, one template
|
|||
|
|
|
|||
|
|
### Subsets
|
|||
|
|
|
|||
|
|
Choice: include or skip each number. No constraint. Base case: index equals list length.
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
def subsets(nums):
|
|||
|
|
result, path = [], []
|
|||
|
|
|
|||
|
|
def backtrack(i):
|
|||
|
|
if i == len(nums):
|
|||
|
|
result.append(path[:])
|
|||
|
|
return
|
|||
|
|
path.append(nums[i]) # include
|
|||
|
|
backtrack(i + 1)
|
|||
|
|
path.pop()
|
|||
|
|
backtrack(i + 1) # skip
|
|||
|
|
|
|||
|
|
backtrack(0)
|
|||
|
|
return result
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Permutations
|
|||
|
|
|
|||
|
|
Same choices as subsets. Constraint: do not reuse a number already in the path. Base case: path length equals list length.
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
def permute(nums):
|
|||
|
|
result, path = [], []
|
|||
|
|
used = [False] * len(nums)
|
|||
|
|
|
|||
|
|
def backtrack():
|
|||
|
|
if len(path) == len(nums):
|
|||
|
|
result.append(path[:])
|
|||
|
|
return
|
|||
|
|
for i in range(len(nums)):
|
|||
|
|
if used[i]:
|
|||
|
|
continue
|
|||
|
|
used[i] = True
|
|||
|
|
path.append(nums[i])
|
|||
|
|
backtrack()
|
|||
|
|
path.pop()
|
|||
|
|
used[i] = False
|
|||
|
|
|
|||
|
|
backtrack()
|
|||
|
|
return result
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Combinations
|
|||
|
|
|
|||
|
|
Same as subsets. Change one line: save the path only when its length equals `k`.
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
def combine(n, k):
|
|||
|
|
result, path = [], []
|
|||
|
|
|
|||
|
|
def backtrack(start):
|
|||
|
|
if len(path) == k:
|
|||
|
|
result.append(path[:])
|
|||
|
|
return
|
|||
|
|
for i in range(start, n + 1):
|
|||
|
|
path.append(i)
|
|||
|
|
backtrack(i + 1)
|
|||
|
|
path.pop()
|
|||
|
|
|
|||
|
|
backtrack(1)
|
|||
|
|
return result
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### N-Queens
|
|||
|
|
|
|||
|
|
Choice: which column to place the queen in the current row. Constraint: no shared column or diagonal. Base case: `n` queens placed. Backtrack: remove the last queen.
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
def solve_n_queens(n):
|
|||
|
|
result, cols = [], []
|
|||
|
|
used_cols, diag1, diag2 = set(), set(), set()
|
|||
|
|
|
|||
|
|
def backtrack(row):
|
|||
|
|
if row == n:
|
|||
|
|
result.append(cols[:])
|
|||
|
|
return
|
|||
|
|
for col in range(n):
|
|||
|
|
if col in used_cols or row - col in diag1 or row + col in diag2:
|
|||
|
|
continue
|
|||
|
|
cols.append(col)
|
|||
|
|
used_cols.add(col); diag1.add(row - col); diag2.add(row + col)
|
|||
|
|
backtrack(row + 1)
|
|||
|
|
cols.pop()
|
|||
|
|
used_cols.remove(col); diag1.remove(row - col); diag2.remove(row + col)
|
|||
|
|
|
|||
|
|
backtrack(0)
|
|||
|
|
return result
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Common mistakes
|
|||
|
|
|
|||
|
|
- You append the `path` list itself instead of a copy. The result fills with empty lists.
|
|||
|
|
- You forget to pop. The path grows and never resets.
|
|||
|
|
- You check constraints after recursion instead of before. You waste time on dead paths.
|
|||
|
|
- You use backtracking when you need only one answer. Use greedy or DP instead.
|
|||
|
|
|
|||
|
|
|