Backtracking seemed like magic to me.
Then I realized every single backtracking problem uses
the same three steps — choose, explore, unchoose.
Once you see it, you can't unsee it.
Here is the complete guide.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
THE CORE IDEA — THREE STEPS EVERY TIME
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Backtracking = build a solution step by step.
At each step: make a choice, explore it, then undo it.
void backtrack(state, choices) {
if (goal reached) {
result.push_back(state); // record the solution
return;
}
for (each valid choice) {
make choice; // add to state
backtrack(...); // explore deeper
undo choice; // restore state — this is the backtrack
}
}The undo step is what makes backtracking different from plain DFS.
You restore state so the next branch starts completely clean.
THINK OF IT LIKE A DECISION TREE:
Each node = a partial solution.
Each edge = one choice made.
Leaf = a complete solution (record it) or a dead end (prune it).
Backtracking = when you hit a leaf, climb back up and try the next edge.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
THERE ARE 4 TYPES OF BACKTRACKING PROBLEMS ON LEETCODE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
TYPE 1 — SUBSETS → include or skip each element
TYPE 2 — PERMUTATIONS → arrange elements in every possible order
TYPE 3 — COMBINATIONS → pick elements to satisfy a condition
TYPE 4 — CONSTRAINTS → place elements satisfying hard rules (N-Queens, Sudoku)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
TYPE 1 — SUBSETS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
When to use:
→ Generate every possible subset of a set
→ Every element is either included or not
Key insight: at each index, two choices — include nums[i] or skip it.
Use a start index so you never go backwards (avoids duplicates).
Record the current state at EVERY level, not just the leaves.
TEMPLATE:
void backtrack(vector<int>& nums, int start,
vector<int>& cur, vector<vector<int>>& res) {
res.push_back(cur); // every partial state is valid
for (int i = start; i < nums.size(); i++) {
cur.push_back(nums[i]); // choose
backtrack(nums, i+1, cur, res); // explore (i+1 = no reuse)
cur.pop_back(); // unchoose
}
}EXAMPLE — Subsets
https://leetcode.com/problems/subsets
vector<vector<int>> subsets(vector<int>& nums) {
vector<vector<int>> res;
vector<int> cur;
backtrack(nums, 0, cur, res);
return res;
}EXAMPLE — Subsets II (input has duplicates)
https://leetcode.com/problems/subsets-ii
Sort first. At the same recursion level, skip if nums[i] == nums[i-1].
This avoids generating the same subset twice.
void backtrack(vector<int>& nums, int start,
vector<int>& cur, vector<vector<int>>& res) {
res.push_back(cur);
for (int i = start; i < nums.size(); i++) {
if (i > start && nums[i] == nums[i-1]) continue; // skip duplicate
cur.push_back(nums[i]);
backtrack(nums, i+1, cur, res);
cur.pop_back();
}
}EXAMPLE — Palindrome Partitioning
https://leetcode.com/problems/palindrome-partitioning
At each position, try every possible prefix.
If it is a palindrome → include it and recurse on the rest.
void backtrack(string& s, int start, vector<string>& cur,
vector<vector<string>>& res) {
if (start == s.size()) { res.push_back(cur); return; }
for (int end = start+1; end <= s.size(); end++) {
string sub = s.substr(start, end-start);
if (isPalindrome(sub)) {
cur.push_back(sub);
backtrack(s, end, cur, res);
cur.pop_back();
}
}
}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
TYPE 2 — PERMUTATIONS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
When to use:
→ Generate all arrangements of elements
→ Order matters — [1,2,3] and [3,2,1] are different answers
Key difference from subsets:
Subsets use a start index (never go backward).
Permutations use a visited array (pick any unused element each step).
TEMPLATE:
void backtrack(vector<int>& nums, vector<bool>& used,
vector<int>& cur, vector<vector<int>>& res) {
if (cur.size() == nums.size()) { res.push_back(cur); return; }
for (int i = 0; i < nums.size(); i++) {
if (used[i]) continue;
used[i] = true;
cur.push_back(nums[i]);
backtrack(nums, used, cur, res);
cur.pop_back();
used[i] = false;
}
}EXAMPLE — Permutations
https://leetcode.com/problems/permutations
Directly apply the template above.
EXAMPLE — Permutations II (input has duplicates)
https://leetcode.com/problems/permutations-ii
// Sort first. Skip nums[i] if it equals nums[i-1] AND nums[i-1]
// is not currently in use. This ensures duplicates are placed
// in the same relative order, avoiding duplicate permutations.
for (int i = 0; i < nums.size(); i++) {
if (used[i]) continue;
if (i > 0 && nums[i] == nums[i-1] && !used[i-1]) continue;
used[i] = true;
cur.push_back(nums[i]);
backtrack(nums, used, cur, res);
cur.pop_back();
used[i] = false;
}EXAMPLE — Letter Combinations of a Phone Number
https://leetcode.com/problems/letter-combinations-of-a-phone-number
Map each digit to its letters. At each step, pick one letter
for the current digit position, then move to the next digit.
void backtrack(string& digits, int idx, string& cur,
vector<string>& res, unordered_map<char,string>& phone) {
if (idx == digits.size()) { res.push_back(cur); return; }
for (char c : phone[digits[idx]]) {
cur.push_back(c);
backtrack(digits, idx+1, cur, res, phone);
cur.pop_back();
}
}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
TYPE 3 — COMBINATIONS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
When to use:
→ Pick elements that satisfy a condition (sum = target, size = k)
→ Order does not matter
→ Elements may or may not be reused
Key difference from subsets:
Subsets collect everything.
Combinations collect only what satisfies a specific condition.
Reuse allowed → pass i (same element can appear again)
No reuse → pass i+1 (each element used at most once)
EXAMPLE — Combination Sum (elements can be reused)
https://leetcode.com/problems/combination-sum
// Sort candidates. Pass i (not i+1) to allow reuse.
// Prune: once candidates[i] > target → break (no point going further).
void backtrack(vector<int>& c, int target, int start,
vector<int>& cur, vector<vector<int>>& res) {
if (target == 0) { res.push_back(cur); return; }
for (int i = start; i < c.size(); i++) {
if (c[i] > target) break; // pruning
cur.push_back(c[i]);
backtrack(c, target-c[i], i, cur, res); // i = reuse allowed
cur.pop_back();
}
}EXAMPLE — Combination Sum II (each element used once, no duplicate combos)
https://leetcode.com/problems/combination-sum-ii
// Sort. Pass i+1 (no reuse). Skip duplicates at the same level.
void backtrack(vector<int>& c, int target, int start,
vector<int>& cur, vector<vector<int>>& res) {
if (target == 0) { res.push_back(cur); return; }
for (int i = start; i < c.size(); i++) {
if (c[i] > target) break;
if (i > start && c[i] == c[i-1]) continue; // skip duplicate
cur.push_back(c[i]);
backtrack(c, target-c[i], i+1, cur, res);
cur.pop_back();
}
}EXAMPLE — Generate Parentheses
https://leetcode.com/problems/generate-parentheses
// At each step, two choices: add '(' or ')'.
// Constraint: open count <= n, close count <= open count.
void backtrack(int n, int open, int close, string& cur,
vector<string>& res) {
if (cur.size() == 2*n) { res.push_back(cur); return; }
if (open < n) {
cur.push_back('(');
backtrack(n, open+1, close, cur, res);
cur.pop_back();
}
if (close < open) {
cur.push_back(')');
backtrack(n, open, close+1, cur, res);
cur.pop_back();
}
}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
TYPE 4 — CONSTRAINT SATISFACTION
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
When to use:
→ Place elements on a board satisfying strict rules
→ "Find all valid configurations"
→ Every placement must be validated before recursing deeper
Keywords: "N-Queens", "Sudoku", "valid placement", "no two conflict"
The key: check ALL constraints BEFORE placing.
If invalid → skip. If valid → place, recurse, undo.
EXAMPLE — N-Queens
https://leetcode.com/problems/n-queens
Place one queen per row. Try every column in that row.
A placement is valid if no queen shares a column, diagonal, or anti-diagonal.
Track three sets to check conflicts in O(1).
void backtrack(int n, int row, vector<string>& board,
vector<vector<string>>& res,
set<int>& cols, set<int>& diag, set<int>& anti) {
if (row == n) { res.push_back(board); return; }
for (int col = 0; col < n; col++) {
if (cols.count(col) || diag.count(row-col) || anti.count(row+col))
continue; // conflicts → skip
board[row][col] = 'Q';
cols.insert(col); diag.insert(row-col); anti.insert(row+col);
backtrack(n, row+1, board, res, cols, diag, anti);
board[row][col] = '.';
cols.erase(col); diag.erase(row-col); anti.erase(row+col);
}
}
vector<vector<string>> solveNQueens(int n) {
vector<vector<string>> res;
vector<string> board(n, string(n, '.'));
set<int> cols, diag, anti;
backtrack(n, 0, board, res, cols, diag, anti);
return res;
}EXAMPLE — Sudoku Solver (Hard)
https://leetcode.com/problems/sudoku-solver
Find the first empty cell. Try digits 1-9.
For each digit, check row, column, and 3x3 box.
If valid → place it, recurse. If recursion fails → undo (backtrack).
bool solve(vector<vector<char>>& board) {
for (int r = 0; r < 9; r++) {
for (int c = 0; c < 9; c++) {
if (board[r][c] != '.') continue;
for (char d = '1'; d <= '9'; d++) {
if (isValid(board, r, c, d)) {
board[r][c] = d;
if (solve(board)) return true;
board[r][c] = '.'; // backtrack
}
}
return false; // no digit worked → tell caller to backtrack
}
}
return true; // no empty cell left → solved
}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
PRUNING — THE DIFFERENCE BETWEEN FAST AND TLE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Backtracking without pruning is brute force. Pruning cuts branches early.
PRUNE 1 — Check constraints BEFORE recursing, not after.
if (choice is invalid) continue; ← prune here, before the call
PRUNE 2 — Sort + break when over the target.
if (candidates[i] > target) break;
No point checking larger values once you've already exceeded.
PRUNE 3 — Skip duplicates at the same recursion level.
if (i > start && nums[i] == nums[i-1]) continue;
Without this, identical elements at the same position
generate identical branches → duplicate results.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
HOW TO IDENTIFY WHICH TYPE — DECISION GUIDE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Read the problem. Ask these questions in order:
Does it have hard placement rules (N-Queens, Sudoku)?
→ YES → Type 4 (Constraint Satisfaction)
Does order matter? ([1,2] ≠ [2,1])
→ YES → Type 2 (Permutations — use visited array)
Does it ask for subsets satisfying a condition (sum, size)?
→ YES → Type 3 (Combinations — use start index)
Can elements repeat? → pass i. Cannot? → pass i+1.
Does it ask for ALL subsets?
→ YES → Type 1 (Subsets — push at every level)
Also ask: does the input have duplicates?
→ YES → sort first, then skip duplicates in the loop.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
COMMON MISTAKES AND HOW TO AVOID THEM
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
MISTAKE 1 — Forgetting to undo the choice
This is the most common bug. If you push, you must pop.
If you set used[i] = true, you must set it back to false.
Without undo, every branch inherits corrupted state from the last branch.
MISTAKE 2 — Passing i instead of i+1 (or vice versa)
Combination Sum (reuse ok) → pass i
Subsets, Combos II (no reuse) → pass i+1
Mixing these gives wrong results silently.
MISTAKE 3 — Not sorting before skipping duplicates
The skip condition i > start && nums[i] == nums[i-1]
only works correctly on sorted input.
Always sort when the problem has duplicates.
MISTAKE 4 — Pushing the same result in multiple places
For subsets → push at every level.
For permutations/combos → push only at the base case.
Mixing this adds wrong or duplicate entries to results.
MISTAKE 5 — Storing a reference instead of a copy
res.push_back(cur) copies correctly for vector.
If you store a pointer or reference to cur, every entry
in res will look the same at the end (the final state of cur).
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
PRACTICE PROBLEMS — IN ORDER
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Type 1 — Subsets
Subsets
https://leetcode.com/problems/subsets
Subsets II
https://leetcode.com/problems/subsets-ii
Palindrome Partitioning
https://leetcode.com/problems/palindrome-partitioning
Type 2 — Permutations
Permutations
https://leetcode.com/problems/permutations
Permutations II
https://leetcode.com/problems/permutations-ii
Letter Combinations of a Phone Number
https://leetcode.com/problems/letter-combinations-of-a-phone-number
Type 3 — Combinations
Generate Parentheses
https://leetcode.com/problems/generate-parentheses
Combination Sum
https://leetcode.com/problems/combination-sum
Combination Sum II
https://leetcode.com/problems/combination-sum-ii
Combinations
https://leetcode.com/problems/combinations
Type 4 — Constraint Satisfaction
N-Queens
https://leetcode.com/problems/n-queens
N-Queens II
https://leetcode.com/problems/n-queens-ii
Sudoku Solver (Hard)
https://leetcode.com/problems/sudoku-solver
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
THE BACKTRACKING CHEAT SHEET — ONE LINE EACH
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
The three steps that never change: choose → explore → unchoose.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Drop a comment if anything is unclear.
I will write a dedicated post on any type you want.