Choose, explore, restore
Picture
Backtracking builds a candidate one decision at a time. It explores what follows from a choice, then restores the working state so the next choice starts from the same place. It is useful when you need constructions or must search choices whose validity depends on a partial solution.
Predict
You are listing subsets using one mutable path list. At a completed subset, should you save path or path.copy()?
Reveal the reasoning
Save a copy. Otherwise many answers refer to the same list, which later choices and undo operations will change. Restoring a working object is different from preserving a completed result.
Go deeper: the invariant is restoration
Assume the input items are distinct.
def subsets(values):
answers, path = [], []
def visit(i):
if i == len(values):
answers.append(path.copy())
return
visit(i + 1) # Exclude this item.
path.append(values[i]) # Include this item.
visit(i + 1)
path.pop() # Restore the caller's path.
visit(0)
return answers
assert subsets(["A", "B"]) == [[], ["B"], ["A"], ["A", "B"]]
assert subsets([]) == [[]]
Each call returns with path exactly as it found it. That local promise prevents a sibling branch from inheriting another branch’s choice. Recursion itself does not automatically undo mutations to shared objects.
There are 2**n outputs, with Θ(n·2**n) total copying time and output storage. Working path and recursion depth use O(n) extra space excluding output. A generator can reduce retained output memory, but still needs to generate every requested result.
Recognition test: are you required to list configurations, or do constraints depend on the partial construction? A choice search is a useful baseline. For existence, stop at the first success. For a count or optimum, ask next whether repeated sufficient states can share an answer.
Analogy limit: backtracking does not always require an explicit pop. Passing a new immutable tuple to each child achieves branch separation through new values, at a copying cost.
Change one rule
The input is ["A", "A"] and identical-looking subsets should appear once. Does this implementation satisfy that output contract?
Check the variation
No. It treats positions as distinct choices and emits ["A"] twice. You need a duplicate policy derived from whether identity belongs to positions or values.
Connect
Pruning avoids branches with a justification. Trie search rejects impossible prefixes. DP can share suffix answers without erasing required distinct outputs.