Every Backtracking Problem Is the Same Three Lines. I Just Couldn't See the Tree.
DEV Community

Every Backtracking Problem Is the Same Three Lines. I Just Couldn't See the Tree.

I avoided backtracking for an embarrassingly long time. I had solved maybe 200 problems and could do graphs, DP, binary search, all of it. But the moment a problem wanted "all subsets" or "every valid arrangement," I went blank.

I had memorized the subsets solution and the permutations solution as two separate spells, and when an interview asked for something slightly different, neither spell fit. The thing that finally fixed it was not another solution. It was redrawing the problem.

Backtracking is DFS with an undo button

Here is the reframe that made everything click. A backtracking problem is a tree of decisions. The root is an empty candidate. Every edge is a choice you make. Every leaf is a finished candidate.

Solving the problem means walking that tree depth-first, building a candidate as you descend and taking it apart as you climb back up. That is the whole thing. Three steps, repeated:

path.push(choice)     # CHOOSE
backtrack(next)       # EXPLORE
path.pop()            # UN-CHOOSE

Choose, explore, un-choose. Subsets, permutations, combination sum, N-queens, Sudoku, word search. They are all this loop. What changes between them is tiny: what counts as a choice, when you stop, and whether you can prune a branch early. The loop itself never changes.

The line everyone forgets is the third one

The choose and the explore feel natural. You pick something, you recurse. Easy. The one that gets dropped is path.pop(). The undo.

And it is brutal because the code still runs. It still produces output of roughly the right shape. It is just quietly wrong, because your choices from one branch leak into the next. You marked a cell used and never unmarked it. You pushed a number and never popped it. Now half your "permutations" are missing elements and you are staring at the recursion trying to find the typo that does not exist. There is no typo. The structure is wrong, and structure is invisible in source code.

The second silent killer is recording by reference:

results.push(path)       # wrong: stores a pointer that keeps mutating
results.push([...path])  # right: stores a frozen copy

The first version gives you an array of the correct length where every entry is the same empty array, because path kept changing after you stored it. People lose twenty minutes to this one in interviews. You cannot debug this by reading. You have to watch it.

Both of those bugs share a property: they are impossible to spot by rereading the code, because the code looks correct. The error only exists in time, across the recursion, as state grows and shrinks.

The day backtracking actually clicked for me was the day I watched the path grow and contract step by step.

  • CHOOSE 1, path is [1].
  • CHOOSE 2, path is [1,2]. Record it.
  • BACKTRACK, path is [1] again.

Seeing the array physically shrink on the un-choose made it obvious why the un-choose is not optional. It is the only thing keeping each branch honest.

Once you have seen one backtracking problem run that way, you stop memorizing solutions. You see the tree, you write the three lines, and you adapt the base case and the pruning to the problem in front of you. N-queens stops being scary because it is the same skeleton as subsets, with one extra check that skips attacked squares before you recurse. That check is where the "back" in backtracking actually pays off, cutting an exponential subtree in a single line.

The practice that builds it

Stop collecting backtracking solutions. You do not need the tenth one. Instead, take the three or four you already have written and label each one the same way:

  • What is a choice here?
  • What is the base case?
  • What gets pruned?

When you can fill in those three blanks for any new problem, you are done. You have the pattern, not the trivia.

It helps enormously to actually see the recursion execute the first few times, with the path and the recorded results visible at every step, instead of tracing it in your head. I wrote up the full pattern with an interactive tree you can step through here: backtracking patterns explained. Watching the choose and un-choose happen is the fastest way to stop fearing the one technique that turns out to be the simplest of them all.

Backtracking is not a category of hard problems. It is one loop you undo. See the tree, and the spells turn back into syntax.

Comments

No comments yet. Start the discussion.