Veröffentlicht · wird verbessert
Backtracking-Anleitung · 1/6
Dieses Kapitel ist vorerst nur auf Englisch verfügbar.
Backtracking is a search technique that builds an answer one piece at a time and, as soon as the choices made so far break a rule, steps back to the previous decision and tries something else. Unlike brute force, which generates every complete candidate and only then checks it, backtracking abandons hopeless paths halfway, so it often solves the same problem with a tiny fraction of the work. This chapter introduces the vocabulary, the basic template, and the situations where backtracking is the right tool.
The easiest way to picture what backtracking explores is a state-space tree. The root is the empty state where nothing has been chosen yet, and each step down the tree adds one more choice. A leaf holds one complete candidate. Here is the tree that generates the permutations of [1, 2, 3]:
[]
/ | \
[1] [2] [3]
/ \ / \ / \
[1,2] [1,3] [2,1] [2,3] [3,1] [3,2]
| | | | | |
[1,2,3] ... [3,2,1]Backtracking walks this tree depth first, but whenever it can tell that a node cannot lead to a valid solution, it skips the entire subtree below that node. This is called pruning.
path.Almost every backtracking routine repeats the same three moves. Choose a candidate and add it to the partial solution, explore recursively from that state, and when the call returns, unchoose by removing what you added. The undo step is what lets every branch share a single path list.
def backtrack(path, choices, result):
if is_solution(path):
result.append(path[:]) # store a copy
return
for c in candidates(path, choices):
if not is_valid(path, c): # prune
continue
path.append(c) # choose
backtrack(path, choices, result) # explore
path.pop() # unchooseIf you write result.append(path) without the copy, every stored entry refers to the same list, which is empty by the time the search finishes. It is one of the most common backtracking bugs.
Splitting on "take this element or leave it" produces the subset tree. With n elements there are 2^n leaves.
def subsets(nums):
result, path = [], []
def dfs(i):
if i == len(nums):
result.append(path[:])
return
path.append(nums[i]) # branch that takes nums[i]
dfs(i + 1)
path.pop()
dfs(i + 1) # branch that skips it
dfs(0)
return result
print(subsets([1, 2, 3]))
# [[1, 2, 3], [1, 2], [1, 3], [1], [2, 3], [2], [3], []]Generate all binary strings of length n with no two adjacent 1s. Refusing to append a 1 right after another 1 is the pruning rule, and it explores far fewer nodes than building every string and filtering afterwards.
def no_adjacent_ones(n):
out = []
def dfs(s):
if len(s) == n:
out.append(s)
return
dfs(s + "0")
if not s.endswith("1"): # never try 1 after 1
dfs(s + "1")
dfs("")
return out
print(no_adjacent_ones(3)) # ['000', '001', '010', '100', '101']n up to about 20) that exponential time is still acceptable.On the other hand, if you only need a count and the same subproblems repeat, DP is usually better, and if the locally best choice always leads to the global optimum, a greedy algorithm wins. The complexity chapter compares these in detail.
0 Kommentare
Anmelden · Melde dich an, um einen Kommentar zu schreiben.
Schreib den ersten Kommentar.