Backtracking Pattern: Algorithmic Intuition, Sub-patterns & C++ Guide
Build the answer one choice at a time; when a choice leads nowhere, undo it and try the next one.
7 sub-patterns Β· 20 questions
What is this pattern?
Backtracking builds an answer one choice at a time. At each step you try a choice, go deeper with it, and when that branch is finished (it worked, or it broke a rule) you undo the choice and try the next one. Every backtracking function has the same three lines in its loop: choose β explore β un-choose.
The calls form a recursion tree: the root has chosen nothing, each edge is one choice, and each leaf is a finished (or dead) attempt. Two things decide the cost:
- The size of the tree. Subsets has 2βΏ nodes, permutations n!. When the question asks for all of them, the output is that big, so no trick can beat it, and the backtracking is already optimal.
- Pruning. When a partial answer already breaks a rule (sum too big, queen attacked, piece not a palindrome), stop that branch right away. Everything below it is never built.
One shared path (vector or string) is passed by reference. It stays correct only because every push has a matching pop after the recursive call.
Walking through a maze with a piece of chalk. At each fork you pick a corridor and draw an arrow. At a dead end you walk back to the last fork, rub out the arrow, and try the next corridor. You never need a second map: the chalk marks always show exactly the path you are on right now.
See it move
Recursion tree for the subsets of [1,2,3]. Blue = the current call's path (pointer call), purple = a recorded subset. The circle text is the path so far.
nums = [1,2,3]step 1/23The first call has chosen nothing yet. In Subsets every call is an answer, so the empty subset [] is recorded right away.
Combination Sum II on [1,1,2,3], target 3. Circle = sum still needed. Blue = current path, purple = answer, red = pruned (too big), orange = duplicate skipped.
candidates = [1,1,2,3], target = 3step 1/18- target
- 3
- need now
- 3
The candidates are sorted: [1,1,2,3]. Sorting puts equal values next to each other (so duplicates are easy to skip) and lets us stop a loop as soon as a value is too big.
How do I recognise it?
"Return all" subsets, permutations, combinations, partitions, paths: the answer is a list of lists.
"Choose k", "sum to target" with each item used once or many times.
Split a string into pieces that each follow a rule (palindromes, IP parts, dictionary words).
Place items under rules: N-Queens, Sudoku, valid parentheses.
Find a path in a grid that may not reuse a cell (Word Search, Rat in a Maze).
Small input limits: n β€ 10β20, a 9Γ9 board, a word of length β€ 15. Exponential time is expected.
Looks like Backtracking, but isn't
Only a count or a best value is asked, not the list itself. The same sub-problem ("ways to make 7 from index 3") repeats, so cache it. Backtracking would rebuild it every time and blow up.
A yes/no answer needs only dp[i] = "prefix of length i can be split". Word Break II asks for every sentence, so it needs backtracking (with a memo on the start index).
A cell is visited once, forever, so visited is never cleared. Backtracking clears the mark on the way out because a different path may use the same cell again.
If a simple rule (always take the earliest-ending interval, the largest coin that fits) can be proven safe, you never need to undo a choice, so there is no tree to explore.
How would I discover it from scratch?
Don't memorise βuse backtrackingβ. Ask these questions in order β the pattern falls out.
Subsets: take or skip this element. Permutations: which unused element goes in this position. Partitioning: where the next piece ends. N-Queens: which column in this row. Write the options as a loop; that loop is the body of your function.
Sub-patterns
Same core idea, different rule for βvalidβ. Learn them in this order.
Subsets
3 questionsEvery element is either in or out, so there are 2βΏ subsets. Build them with a start index; every call is one subset.
Permutations
3 questionsFill positions one at a time with an element not used yet. The order matters, so there are n! answers.
Combinations / choose K
3 questionsChoose exactly k items where order doesn't matter: pick them in increasing order, and prune when there aren't enough items left.
Combination sum
2 questionsPick items that add up to a target. Sorting lets you stop the loop as soon as one item is too big.
String partitioning
3 questionsCut off a valid first piece of the string, then recurse on the rest; a full cut of the string is one answer.
Constraint satisfaction
3 questionsPlace items one by one, and before each placement check the rules so a bad partial answer is dropped early.
Grid / path search
3 questionsWalk from cell to cell (or node to node). Mark a cell when it joins the path and unmark it when you leave, so other paths can use it.
Don't mix these up
Is [1,2] the same answer as [2,1]? Then pick only to the right with start. If they are different answers, loop from 0 and skip used elements. Using start for permutations returns only the sorted order; using used[] for subsets returns every subset in every order.
Combination Sum lets a number be picked again, so the next call starts at the same index. Combination Sum II and III don't, so it starts at i + 1. On [2,3,6,7], target 7, i + 1 misses [2,2,3].
The skip is only for equal values tried at the same depth. i > 0 also skips the second 2 when it is the next item deeper in the path, so [2,2] is never built on [1,2,2].
Counting islands visits each cell once, so the mark stays. Listing paths must clear the mark on the way out, or a second path through the same cell is never found.
If the question wants the list, you must build each item, so backtrack. If it only wants how many or the best, the same sub-problems repeat and DP counts them without building them.
Code templates (C++)
Subsets (with duplicate skip)
Record the path on entry (every node is a subset), then loop i from start. Sorting + i > start && nums[i] == nums[i-1] skips a value already tried at this depth.
// Subsets: every call is one subset; the loop picks the NEXT element to add, always to the right.
// Sort first and skip equal neighbours at the same depth to avoid duplicate subsets (Subsets II).
void build(const vector<int>& nums, int start, vector<int>& path, vector<vector<int>>& out) {
out.push_back(path); // every node of the tree is an answer
for (int i = start; i < (int)nums.size(); i++) {
if (i > start && nums[i] == nums[i - 1]) continue; // same value already tried at this depth
path.push_back(nums[i]); // choose
build(nums, i + 1, path, out); // explore: only elements to the right
path.pop_back(); // un-choose
}
}
vector<vector<int>> subsetsWithDup(vector<int> nums) {
sort(nums.begin(), nums.end()); // equal values side by side
vector<vector<int>> out;
vector<int> path;
build(nums, 0, path, out);
return out;
}Permutations (used[] array)
Loop from 0 every time and skip used elements. With duplicates, take equal copies only left to right: skip when the copy to the left is not used.
// Permutations: fill positions one by one with any element not used yet.
// With duplicates: sort, and use equal copies only in left-to-right order (skip if the left copy is unused).
void build(const vector<int>& nums, vector<bool>& used, vector<int>& path, vector<vector<int>>& out) {
if (path.size() == nums.size()) { out.push_back(path); return; }
for (int i = 0; i < (int)nums.size(); i++) { // any position: loop from 0, not from start
if (used[i]) continue;
if (i > 0 && nums[i] == nums[i - 1] && !used[i - 1]) continue; // duplicate branch
used[i] = true; path.push_back(nums[i]); // choose
build(nums, used, path, out); // explore
path.pop_back(); used[i] = false; // un-choose
}
}
vector<vector<int>> permuteUnique(vector<int> nums) {
sort(nums.begin(), nums.end());
vector<vector<int>> out;
vector<int> path;
vector<bool> used(nums.size(), false);
build(nums, used, path, out);
return out;
}Choose K in increasing order
The start index keeps each group in increasing order, so it is built once. The loop bound n - need + 1 stops when too few numbers are left to fill the group.
// Choose K: pick items in increasing order (start index), so each group appears once.
// Prune when the numbers left can't fill the remaining slots.
void build(int n, int k, int start, vector<int>& path, vector<vector<int>>& out) {
if ((int)path.size() == k) { out.push_back(path); return; }
int need = k - path.size();
for (int i = start; i <= n - need + 1; i++) { // i > n-need+1 leaves too few numbers
path.push_back(i);
build(n, k, i + 1, path, out); // next pick must be larger
path.pop_back();
}
}
vector<vector<int>> combine(int n, int k) {
vector<vector<int>> out;
vector<int> path;
build(n, k, 1, path, out);
return out;
}Combination sum (reuse or not)
Sort, then break when a candidate is bigger than what is left. Reuse allowed: recurse on i. No reuse: recurse on i + 1 and skip equal neighbours.
// Combination sum: target-driven. Sorted candidates let us stop as soon as one is too big.
// reuse allowed (LC 39): recurse on i no reuse (LC 40): recurse on i+1 and skip equal neighbours
void build(const vector<int>& c, int start, int remaining, bool reuse,
vector<int>& path, vector<vector<int>>& out) {
if (remaining == 0) { out.push_back(path); return; }
for (int i = start; i < (int)c.size(); i++) {
if (c[i] > remaining) break; // sorted: everything after is too big too
if (!reuse && i > start && c[i] == c[i - 1]) continue;
path.push_back(c[i]);
build(c, reuse ? i : i + 1, remaining - c[i], reuse, path, out);
path.pop_back();
}
}
vector<vector<int>> combinationSum(vector<int> candidates, int target, bool reuse) {
sort(candidates.begin(), candidates.end());
vector<vector<int>> out;
vector<int> path;
build(candidates, 0, target, reuse, path, out);
return out;
}String partitioning
Try every end for the next piece, recurse only if the piece is valid. A table ok[i][j] built once in O(nΒ²) makes each palindrome check O(1).
// String partitioning: cut off a valid first piece s[start..end], recurse on the rest.
// Here the rule is "piece is a palindrome", answered in O(1) by a table built once in O(n^2).
void build(const string& s, int start, const vector<vector<bool>>& ok,
vector<string>& pieces, vector<vector<string>>& out) {
if (start == (int)s.size()) { out.push_back(pieces); return; } // the whole string is cut
for (int end = start; end < (int)s.size(); end++) {
if (!ok[start][end]) continue; // invalid piece: prune this branch
pieces.push_back(s.substr(start, end - start + 1));
build(s, end + 1, ok, pieces, out);
pieces.pop_back();
}
}
vector<vector<string>> partition(const string& s) {
int n = s.size();
vector<vector<bool>> ok(n, vector<bool>(n, false)); // ok[i][j]: s[i..j] is a palindrome
for (int i = n - 1; i >= 0; i--)
for (int j = i; j < n; j++)
ok[i][j] = s[i] == s[j] && (j - i < 2 || ok[i + 1][j - 1]);
vector<vector<string>> out;
vector<string> pieces;
build(s, 0, ok, pieces, out);
return out;
}Placement with O(1) rule checks
One queen per row. Columns, r + c diagonals and r β c + n β 1 anti-diagonals each get a bool array; set all three on choose, clear all three on un-choose.
// Constraint satisfaction: place one item per step (a queen per row), check the rules in O(1)
// with "taken" arrays, and undo all of them on the way back.
void place(int r, int n, vector<string>& board, vector<bool>& col, vector<bool>& diag,
vector<bool>& anti, vector<vector<string>>& out) {
if (r == n) { out.push_back(board); return; } // every row has a queen
for (int c = 0; c < n; c++) {
if (col[c] || diag[r + c] || anti[r - c + n - 1]) continue; // attacked: prune
board[r][c] = 'Q'; col[c] = diag[r + c] = anti[r - c + n - 1] = true;
place(r + 1, n, board, col, diag, anti, out);
board[r][c] = '.'; col[c] = diag[r + c] = anti[r - c + n - 1] = false;
}
}
vector<vector<string>> solveNQueens(int n) {
vector<string> board(n, string(n, '.'));
vector<bool> col(n), diag(2 * n - 1), anti(2 * n - 1);
vector<vector<string>> out;
place(0, n, board, col, diag, anti, out);
return out;
}Grid path: mark in, unmark out
Overwrite the cell with # while it is on the path, and restore it after the four calls. Return true straight up the chain once the word is found.
// Grid path: mark the cell on the way in, unmark on the way out,
// so other paths may still pass through it later (Word Search).
bool dfs(vector<vector<char>>& g, const string& word, int i, int r, int c) {
if (i == (int)word.size()) return true; // every letter matched
if (r < 0 || c < 0 || r >= (int)g.size() || c >= (int)g[0].size() || g[r][c] != word[i])
return false; // off the grid, used, or wrong letter
char saved = g[r][c];
g[r][c] = '#'; // mark: in use on this path
bool found = dfs(g, word, i + 1, r + 1, c) || dfs(g, word, i + 1, r - 1, c) ||
dfs(g, word, i + 1, r, c + 1) || dfs(g, word, i + 1, r, c - 1);
g[r][c] = saved; // unmark: free for other paths
return found;
}
bool exist(vector<vector<char>>& board, const string& word) {
for (int r = 0; r < (int)board.size(); r++)
for (int c = 0; c < (int)board[0].size(); c++)
if (dfs(board, word, 0, r, c)) return true;
return false;
}