Universal Backtracking Template
All backtracking problems follow the same structure. Fill in the three labeled sections.
C++
void backtrack(/* state */, vector<vector<int>>& result, vector<int>& current) {
// BASE CASE — current is a complete valid solution
if (/* done */) {
result.push_back(current);
return;
}
// ITERATE over all choices at this point
for (int choice : /* candidates */) {
// PRUNE — skip invalid choices
if (/* invalid(choice) */) continue;
// CHOOSE — add to current path
current.push_back(choice);
// EXPLORE — recurse with updated state
backtrack(/* updated state */, result, current);
// UN-CHOOSE — undo to restore state (backtrack!)
current.pop_back();
}
}
// The pattern is: choose → explore → un-choose
// Pruning is optional but critical for performance
Interactive: Backtracking Subsets
Watch backtracking generate subsets of [1, 2, 3]. Each step either chooses an element (highlighted) or backtracks (crossed). Every partial path is a valid subset.
Practice Problems
#78SubsetsMedium
Return all subsets (power set) of a list of unique integers.
Pattern: At each index, choose to include or not include it. Result is added at each level, not just leaf. Start from increasing index to avoid repeats.
C++
void bt(vector<int>& nums, int start, vector<int>& cur, vector<vector<int>>& res) {
res.push_back(cur); // every partial path is a valid subset
for(int i=start; i<nums.size(); i++) {
cur.push_back(nums[i]);
bt(nums, i+1, cur, res);
cur.pop_back();
}
}
vector<vector<int>> subsets(vector<int>& nums) {
vector<vector<int>> res; vector<int> cur;
bt(nums, 0, cur, res); return res;
}
// Time: O(2ⁿ) Space: O(n)
#46PermutationsMedium
Return all permutations of distinct integers.
Pattern: At each step, pick any unused number. Use visited array or swap-based approach.
C++
void bt(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]);
bt(nums,used,cur,res);
used[i]=false; cur.pop_back();
}
}
vector<vector<int>> permute(vector<int>& nums) {
vector<vector<int>> res;
vector<int> cur; vector<bool> used(nums.size(),false);
bt(nums,used,cur,res); return res;
}
// Time: O(n! * n) Space: O(n)
#39Combination SumMedium
Find all combinations from candidates that sum to target. Same number can be used multiple times.
Pattern: Backtracking with same index allowed (not i+1). Prune when remaining < 0. Sort first for early termination.
C++
void bt(vector<int>& c, int start, int rem, vector<int>& cur, vector<vector<int>>& res) {
if(rem==0) { res.push_back(cur); return; }
for(int i=start; i<c.size() && c[i]<=rem; i++) {
cur.push_back(c[i]);
bt(c, i, rem-c[i], cur, res); // i not i+1 (reuse allowed)
cur.pop_back();
}
}
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
sort(candidates.begin(),candidates.end());
vector<vector<int>> res; vector<int> cur;
bt(candidates,0,target,cur,res); return res;
}
#17Letter Combinations of a Phone NumberMedium
Given digits 2-9, return all possible letter combinations (T9 mapping).
Pattern: At each digit, try all mapped letters. Recurse on next digit.
C++
void bt(string& digits, int i, string& cur, vector<string>& res, vector<string>& mp) {
if(i==digits.size()) { res.push_back(cur); return; }
for(char c : mp[digits[i]-'2']) {
cur.push_back(c);
bt(digits,i+1,cur,res,mp);
cur.pop_back();
}
}
vector<string> letterCombinations(string digits) {
if(digits.empty()) return {};
vector<string> mp = {"abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"};
vector<string> res; string cur;
bt(digits,0,cur,res,mp); return res;
}
#79Word SearchMedium
Search for a word in a 2D grid. Letters must be adjacent (4-directional) and not reused.
Pattern: DFS backtracking on grid. Mark cell visited by overwriting, restore on backtrack. Prune immediately if letter doesn't match.
C++
bool dfs(vector<vector<char>>& g, string& w, int r, int c, int k) {
if(k==w.size()) return true;
if(r<0||r>=g.size()||c<0||c>=g[0].size()||g[r][c]!=w[k]) return false;
char tmp=g[r][c]; g[r][c]='#'; // mark visited
bool found = dfs(g,w,r+1,c,k+1)||dfs(g,w,r-1,c,k+1)||
dfs(g,w,r,c+1,k+1)||dfs(g,w,r,c-1,k+1);
g[r][c]=tmp; // restore
return found;
}
bool exist(vector<vector<char>>& grid, string word) {
for(int r=0;r<grid.size();r++)
for(int c=0;c<grid[0].size();c++)
if(dfs(grid,word,r,c,0)) return true;
return false;
}
// Time: O(m*n*4^L) Space: O(L)
#131Palindrome PartitioningMedium
Partition string s such that every substring is a palindrome. Return all such partitions.
Pattern: Backtracking where at each position we try all substrings starting here. If palindrome, add to path and recurse. Prune non-palindromes.
C++
bool isPalin(string& s, int l, int r) {
while(l<r) if(s[l++]!=s[r--]) return false;
return true;
}
void bt(string& s, int start, vector<string>& cur, vector<vector<string>>& res) {
if(start==s.size()) { res.push_back(cur); return; }
for(int end=start; end<s.size(); end++) {
if(!isPalin(s,start,end)) continue;
cur.push_back(s.substr(start,end-start+1));
bt(s,end+1,cur,res);
cur.pop_back();
}
}
vector<vector<string>> partition(string s) {
vector<vector<string>> res; vector<string> cur;
bt(s,0,cur,res); return res;
}
#51N-QueensHard
Place n queens on n×n chessboard so no two queens attack each other. Return all valid arrangements.
Pattern: Place one queen per row. Track which columns, diagonals (row-col), and anti-diagonals (row+col) are occupied. Each row: try each column, skip if attacked.
C++
void bt(int n, int row, vector<string>& board,
set<int>& cols, set<int>& diag, set<int>& anti,
vector<vector<string>>& res) {
if(row==n) { res.push_back(board); return; }
for(int c=0;c<n;c++) {
if(cols.count(c)||diag.count(row-c)||anti.count(row+c)) continue;
board[row][c]='Q';
cols.insert(c); diag.insert(row-c); anti.insert(row+c);
bt(n,row+1,board,cols,diag,anti,res);
board[row][c]='.';
cols.erase(c); diag.erase(row-c); anti.erase(row+c);
}
}
vector<vector<string>> solveNQueens(int n) {
vector<string> board(n, string(n,'.'));
vector<vector<string>> res;
set<int> cols,diag,anti;
bt(n,0,board,cols,diag,anti,res);
return res;
}
// Time: O(n!) Space: O(n)
Pruning is everything. The difference between TLE and AC is pruning. Before recursing, check if the current path can possibly lead to a valid answer. If not, return immediately.
Deeper Understanding
Why Backtracking Is Not Brute Force
Pruning Cuts the Search Space Exponentially
Brute force: try every possible assignment
For N-Queens on an 8×8 board: 8^8 = 16,777,216 candidate placements. Backtracking cuts this to ~15,000 nodes — a 1000× reduction from pruning invalid columns and diagonals early.
Pruning point: prune as early as possible
Check constraints before recursing, not after. If placing a queen at (row, col) is already invalid, don't recurse into that subtree at all — saves the entire sub-tree cost.
Constraint propagation (advanced)
After placing a queen, immediately mark its row, column, and diagonals as off-limits. This narrows candidates for future rows from N down to much fewer — the essence of sudoku solvers.
Classic Mistake: Forgetting to Un-choose
The bug: mutating state without restoring it
If you
push_back(choice) before recursing but forget to pop_back() after, the cur vector accumulates all choices ever made — not just the current path. Each branch sees a polluted state.The fix: always mirror choose with un-choose
Every mutation before the recursive call needs a matching undo after it. This is why the pattern is always:
cur.push_back(x); bt(...); cur.pop_back(); — symmetric and explicit.Global vs local state
Prefer passing
cur by reference and restoring it. Passing by value (copying the path each call) is correct but O(n) space per level — fine for small n, but avoid when the path is large.Recognizing When to Use Backtracking
Signal: "find all" or "enumerate all valid"
Subsets, permutations, combinations, all paths in a maze — any problem asking for an exhaustive list of solutions is a backtracking candidate.
Signal: constraint satisfaction
N-Queens, Sudoku, word search — problems where you must satisfy constraints at every step and a partial assignment can be declared invalid early.
Don't use if: optimal single answer, no enumeration needed
If you only need one optimal value (max profit, min cost), DP or greedy is better. Backtracking explores all possibilities — avoid if overlapping subproblems exist.