mirror of
https://github.com/prdlk/leetcode.git
synced 2026-11-02 21:22:43 +00:00
164 lines
4.7 KiB
Plaintext
164 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.
|
||
|
||
|