2Brute2Best
πŸ”₯0

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

Remember this Β· Backtracking
Trigger
"All subsets / permutations / combinations", place items under rules (N-Queens, Sudoku), find a path in a grid.
Think
At each step, what are my choices, and how do I undo a choice?
Key question
What is my state, when is it complete, and when do I prune?
A

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.

B

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.

Choose, explore, un-choosenums = [1,2,3]step 1/23
recursion tree
[]call
nums
0
1
2
1
2
3
β–²start
path
0
Β·
subsets so far
[]
Record []

The 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.

Pruning: break early, skip duplicatescandidates = [1,1,2,3], target = 3step 1/18
recursion tree (circle = sum still needed)
3call
sorted
0
1
2
3
1
1
2
3
path
0
Β·
Rules
target
3
need now
3
answers
[ ]
Sort first

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.

C

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

"How many ways" / "minimum number of …" with n up to 1000s
β†’ Dynamic Programming

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.

Can the string be split into dictionary words? (Word Break I)
β†’ Dynamic Programming

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).

Count islands / fill a region in a grid
β†’ Graph traversal (DFS/BFS)

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.

Pick items to maximise a total, one pass is enough
β†’ Greedy

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.

D

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.

E

Sub-patterns

Same core idea, different rule for β€œvalid”. Learn them in this order.

F

Don't mix these up

Start index (combinations) vs used[] array (permutations)

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.

Recurse on i (reuse) vs Recurse on i + 1 (no reuse)

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].

Skip duplicates with i > start vs Skip duplicates with i > 0

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].

Backtracking unmark vs Graph-traversal visited

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.

Backtracking (list them all) vs DP (count them / best one)

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.

G

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-template.cpp
// 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-template.cpp
// 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-template.cpp
// 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-template.cpp
// 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).

partition-template.cpp
// 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.

placement-template.cpp
// 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-template.cpp
// 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;
}