Backtracking
Build an answer one choice at a time, and undo the last choice when it leads nowhere so you can try the next one.
O(k^n × n)Space O(n)12 practice problemsStep-through pictureThe idea
Some problems ask for every answer that follows a rule, such as every password of length 3 with no letter twice in a row. You can't find those answers with one formula. You have to try choices.
Backtracking tries them in an orderly way. It keeps a path: the choices made so far. It adds one choice, then goes deeper to add the next. When the path is complete, it saves a copy. When a choice breaks the rule, it skips that choice. After going deeper, it removes the last choice again, which is the step back that gives the method its name, and tries the next choice in the same spot.
This visits every possible answer exactly once, and it never builds a path that already broke the rule. That second part is what makes it faster than listing every possible string and checking each one at the end.
An everyday example
Walking through a maze with a ball of string
You enter a maze holding a ball of string, and you unroll it as you walk. At every fork you take the first path you haven't tried. If you hit a dead end, you walk back along the string, rolling it up, to the last fork, and try the next path there. When you reach an exit, you write down the route your string shows. Because you always roll the string back up, the string shows exactly the route from the entrance to where you stand.
| In the example | In the code |
|---|---|
| The string from the entrance to where you stand | The path array |
| Taking a new path at a fork | path.push(choice) then explore() |
| Rolling the string back up after a dead end | path.pop() |
| A path you can see is blocked, so you don't walk in | continue when a choice breaks the rule |
| Writing down the route at an exit | results.push(path.join("")) |
From the slow way to backtracking
- Say you want every string of length
nmade from the lettersu,v, andw, with no letter twice in a row. The obvious way is to list every string of lengthnfirst, then throw away the bad ones. With 3 letters that is3^nstrings: 59,049 of them forn = 10, and each one takesnsteps to build and check. - Most of that work is wasted. Once a string starts with
uu, nothing you add after it can fix it. Yet the obvious way still builds all 6,561 strings of length 10 that begin withuu, one by one, only to throw each away. The useful fact is this: a broken start stays broken, so the rule can be checked on a partial string, not only on a finished one. - So build the string one letter at a time, and check the rule each time you add a letter. If a letter breaks the rule, don't add it, and never look at anything that would grow from it. This turns the flat list of strings into a tree of choices: the root is the empty string, each child adds one letter, and one skipped letter cuts off a whole branch of the tree.
- Walking that tree needs a memory of where you are. Keep one array,
path, for the letters chosen so far. Going down a branch ispath.push(choice). Coming back up ispath.pop(), which putspathback the way it was, so the next letter at the same spot starts from the same place. A recursive function,explore, handles one level of the tree per call. That loop of push, explore, pop, with a skip for broken choices, is backtracking.
How it works
- Keep a
patharray with the choices made so far, and aresultsarray for the finished answers. - Write a recursive function,
explore, that tries to extend thepathby one more choice. - In
explore, first check if thepathis complete. If it is, save a copy intoresultsand return. - Otherwise loop over every
choice. Skip any choice that would break the rule right away. - For a valid choice, add it with
path.push(choice)and callexplore()to fill the next spot. - When that call returns, undo the choice with
path.pop(), so the next loop step starts from the samepath. - Start with one call,
explore(), on an emptypath, and returnresultsat the end.
Watch it run
This picture runs the code below on a real input. Press Step to move one step at a time, or Play to watch. The highlighted line is the one that just ran. Change the input to see other cases.
See every step at onceThe same run, laid out side by side
The code
Point at a note to light up the lines it explains.
function noRepeatStrings(choices, length) {const results = [];const path = [];function explore() {if (path.length === length) {results.push(path.join(""));return;}for (const choice of choices) {if (path[path.length - 1] === choice) continue;path.push(choice);explore();path.pop();}}explore();return results;}
How fast it is
| Time | O(k^n × n) |
|---|---|
| Extra memory | O(n) |
With k choices and length n, there can be up to k^n paths, and each saved answer takes n steps to copy. Apart from the output, the only memory is path and the chain of calls, both at most n deep.
- Best case
O(1)The rule cuts everything at once, such as
choices = ["a"]withlength = 3:ais pushed, the next spot skipsa, and the search ends after 2 calls with no answers. - Average case
O(n × k × (k-1)^(n-1))With this rule, each spot after the first has
k - 1allowed letters, so there arek × (k-1)^(n-1)answers, each copied innsteps. Fork = 3andn = 10the search makes 3,070 calls and saves 1,536 strings. - Worst case
O(k^n × n)A rule that never rejects anything: every one of the
k^nstrings is built and copied. This is the page's general bound.
Common mistakes
- Saving
pathitself withresults.push(path). Every saved entry is the same array, which ends up empty. Save a copy:[...path]orpath.join(""). - Forgetting
path.pop()after the deeper call. The path keeps growing, and later answers contain old choices. - Checking the rule only when the path is complete. The answer is right but the search builds many paths that were already broken. Check as early as you can.
- Using backtracking when the question only wants a count of a huge number of answers. The count can be found much faster with dynamic programming.
When to use it
Look for these clues in a problem:
- The question asks for all answers, not just one number: "list every", "generate all", "return every way".
- Each answer is built from a series of small choices, such as one letter, one item, or one cell at a time.
- The input is small, often 10 to 20 items, because the number of answers can grow very fast.
- There is a rule that lets you reject a partial answer early, before it's complete.
Check yourself
Answer each one in your head first, then open it.
Why can one shared path array serve every call?
Because each call undoes exactly what it did: one push before the deeper call and one pop after it. So when control returns to a loop, path is the same as before that loop step.
With choices = ["u", "v", "w"] and length = 2, which strings are never built?
uu, vv, and ww. Each is cut at its second letter by the continue, which is why the trace has 3 skip rows.
What happens if you delete the path.pop() line?
path keeps growing past length, so path.length === length is never true again and the recursion goes deeper until the engine throws a RangeError for the call stack.
Why must the time be at least the number of answers times n?
Every answer has n letters, and each one is copied into results with path.join(""). Just writing the output takes that long.
Practice problems
Stuck on one? Tuto is in the playground too, and gives you hints, never the answer.
0 of 12 solved. Solve one to earn the bronze medal, half for silver, and all of them for gold.
Start practicingmedium
Go further
You don't need these to start practicing. Open one when you want more.
Why it works
The reasoning that makes it always right
Backtracking keeps one rule. When explore is called, path holds a valid start of an answer, and when that call returns, path is exactly as it was at the call. The first part holds because a choice is only pushed after it passes the check. The second part holds because every push is matched by one pop right after the deeper call, and the deeper calls keep the same rule.
Every valid answer is found. Take any valid string, such as "wu". At the first spot the loop tries w, which passes the check, so the search goes down that branch. At the second spot it tries u, which passes too, so "wu" is saved. A choice is only skipped when it already breaks the rule, and then every string that would grow from it breaks the rule too, so no valid string is ever lost.
No answer is saved twice, and the search always ends. Two different strings differ at some first position, so they live in different branches of the tree and are reached by different loop steps. The tree has at most length levels, and each level loops over a fixed list of choices, so there are only a limited number of calls. When the top-level loop ends, every branch has been tried.
Going deeper
3 more ideas
Why the undo step keeps everything correct
Every call to explore must leave path exactly as it found it. A call adds one item, goes deeper, and removes the same item. The deeper calls follow the same rule, so when control returns to a loop, path holds the same choices as before the loop step. That is why one shared array is enough for the whole search.
Why skipping early saves so much work
Without the rule, 2 letters and length 3 give 2 × 2 × 2 = 8 strings to check. The skip on the second letter cuts every branch that starts with aa or bb before it grows. With 3 letters and length 10 the difference is 59,049 strings against 1,536. Rejecting a bad partial answer removes every answer that would grow from it.
How long it takes
Backtracking is slow by nature: the time is about the number of answers times the length of each one, because each answer is built and copied. That is fine when the input is small. When a problem only asks how many answers exist, or for the best one, other methods such as dynamic programming are often faster.
Work it by hand
Every step in a table, with why
Every 2-letter string from three letters, with no letter twice in a row. Each row is one turn of the for loop, at whichever depth the search is.
choices = ["u", "v", "w"], length = 2
| Step | path before | choice | path after | What happens | Why |
|---|---|---|---|---|---|
| 1 | [] | u | [u] | push, go deeper | path is empty, so there is no last letter to clash with. Push u and call explore() to fill the second spot. |
| 2 | [u] | u | [u] | skip | u equals the last letter of path, so continue skips it. Nothing that starts with uu is ever built. |
| 3 | [u] | v | [u, v] | save "uv" | path.length is now 2, so the deeper call saves "uv" and returns at once. Then path.pop() puts path back to [u]. |
| 4 | [u] | w | [u, w] | save "uw" | Same spot, next choice. Save "uw" and pop back to [u]. The loop at this depth is done, so this call returns and its caller pops u. |
| 5 | [] | v | [v] | push, go deeper | Back at the top level with an empty path. Now v is tried as the first letter. |
| 6 | [v] | u | [v, u] | save "vu" | A letter may appear again later, just not right after itself, so u after v is fine. |
| 7 | [v] | v | [v] | skip | v would repeat the last letter, so it is skipped. |
| 8 | [v] | w | [v, w] | save "vw" | Save "vw", pop to [v], and return, so the top level pops v. |
| 9 | [] | w | [w] | push, go deeper | The last first letter to try. |
| 10 | [w] | u | [w, u] | save "wu" | Save "wu" and pop back to [w]. |
| 11 | [w] | v | [w, v] | save "wv" | Save "wv" and pop back to [w]. |
| 12 | [w] | w | [w] | skip | Here the skip is the very last loop step. The inner loop ends, w is popped, and the top-level loop ends too. |
It returns ["uv", "uw", "vu", "vw", "wu", "wv"]: 6 of the 3 × 3 = 9 possible strings. The 3 skipped rows are the strings uu, vv, and ww, which were never built. The search ends when the top-level loop has tried all three first letters.
Compared with other ways
When to pick something else
| Approach | Time | Extra memory | Choose it when |
|---|---|---|---|
| Backtracking | O(n · 2^n) | O(n) | You need every valid string. With 3 letters the rule leaves 3 × 2^(n-1) answers, and the search builds only those plus a few dead ends. |
| Build every string, then filter | O(n · 3^n) | O(n) | Only when the rule can't be checked until the string is complete. Otherwise it builds every uu... string just to throw it away. |
| Count with a formula or dynamic programming | O(n) | O(1) | The question only asks how many answers there are. Multiply k by k - 1 for each later spot instead of listing anything. |
Extra memory: O(n) besides the output: path holds at most length letters, and the call chain is at most length + 1 calls deep. The results array grows with the number of answers, which can be far larger.
How the steps grow
- Backtracking
O(n · 2^n) - Build every string, then filter
O(n · 3^n) - Count with a formula or dynamic programming
O(n)
| Approach | n = 10 | n = 1,000 | n = 1,000,000 |
|---|---|---|---|
| Backtracking | 10,240 | more than a quadrillion | more than a quadrillion |
| Build every string, then filter | 590,490 | more than a quadrillion | more than a quadrillion |
| Count with a formula or dynamic programming | 10 | 1,000 | 1 million |
Inputs that need care
5 tricky inputs
length = 0path.length === lengthis true on the very first call, so it savespath.join(""), the empty string, and returns[""]. That is correct: there is exactly one string of length 0.choices = [],length = 2- The
forloop never runs, so nothing is saved and it returns[]. No special check is needed. - One letter,
choices = ["a"],length = 3 - The second spot can only try
a, which repeats the last letter, so the search dead-ends and returns[]. - Repeated letters,
choices = ["u", "u", "v"],length = 2 - Both copies of
uare tried as the first letter, so the result is["uv", "uv", "vu", "vu"]. Remove repeats first with[...new Set(choices)]if each answer should appear once. - A big output: 10 letters and
length = 8 - There are
10 × 9^7 = 47,829,690valid strings. The code is correct but the output alone won't fit in a browser tab. If only the count is needed, computek × (k-1)^(n-1)instead of listing.
Variations and follow-ups
Twists interviewers add
Stop at the first answer
Make explore return true when it finds a complete answer, and return at once when a deeper call returns true. The search ends as soon as one answer exists. Puzzle solvers use this, since they only need one solution.
Count instead of collect
Replace results.push(...) with count++. This saves memory, but the time is still the number of answers. When only the count matters, look for a formula or dynamic programming, which can count without visiting each answer.
Branch and bound
When you want the best answer by some score, keep the best score found so far. Before going deeper, estimate the best score this branch could still reach, and skip the branch if it can't beat the current best. This is backtracking with a smarter skip rule.
An explicit stack instead of recursion
Each call can be stored as a small record (how long path is, and which choice to try next) on your own stack array. The search order stays the same, and very deep searches no longer depend on the engine's call stack limit.
In an interview
Spot it, explain it, answer follow-ups
- Spot it from words like "generate all", "list every", or "return all valid", together with small limits such as
n <= 10. A small limit is a hint that an exponential search is expected. - Explain it as three moves: choose (
path.push), explore (the recursive call), and un-choose (path.pop). Then say where the rule check sits and why it sits before the push, not at the end. - Test by hand with
length = 0, a single letter, and a tiny case where one skip happens, like the trace above. Most bugs are a missingpopor savingpathwithout a copy. - Expect "what is the time?". Answer with the number of tree nodes times the cost of copying an answer, and point out that the output size is a floor no method can beat when every answer must be listed.
- Expect "can you make it faster?". The honest answer: move the rule check earlier so more branches are cut, or, if only a count or a best value is needed, switch to dynamic programming.
Where frontend code uses it
The same idea in real UI code
- JavaScript's
RegExpengine in V8 is a backtracking engine: when a part of a pattern fails, it steps back and tries another way to match. Patterns like(a+)+$can make it try a huge number of ways, which is how a bad regular expression can freeze a page. - Design systems often test every combination of a component's props (size, variant, state). Generating those combinations is a small backtracking search.
- Form builders and checkout flows with dependent options (a size that only exists for some colors) can list valid choices by building one option at a time and skipping invalid ones.