Recursion is delegation. Instead of solving the whole problem, you solve a smaller version and combine results. Backtracking is systematic trial and error—try a choice, explore all consequences, undo the choice, try the next.Analogy: Imagine you manage a team. Instead of doing all the work yourself, you hand a slightly smaller version of the task to a subordinate, who hands an even smaller version to their subordinate, until someone gets a trivially small task and just does it. The answers bubble back up the chain. That is recursion. Backtracking adds: “if the subordinate reports failure, try a different subordinate.”
Pattern Recognition Signals:
“Generate all combinations/permutations”
“Find all valid configurations”
Problem has natural substructure (solve for n-1, then n)
Constraints are small (n ≤ 20 for backtracking, n ≤ 10 for permutations)
Problem: Generate all 2^n subsets of an array.Approach: For each element, make two choices: include it or don’t.Analogy: Imagine standing at a buffet with n dishes. At each dish, you decide “take it” or “skip it.” Every possible combination of takes and skips is a subset. With n dishes, you make n binary decisions, giving 2^n total subsets.Worked example: nums = [1, 2, 3]
Problem: Generate all C(n, k) combinations of size k.Key Insight: Like subsets, but only output when we have exactly k elements.The pruning line is critical: current.size() + (n - start + 1) < k checks whether there are enough remaining elements to fill the combination. Without this, you explore branches that can never lead to a valid combination — wasting exponential time. For example, if you need 5 elements but only 3 remain, there is no point continuing.
vector<vector<int>> combinations(int n, int k) { vector<vector<int>> result; vector<int> current; function<void(int)> backtrack = [&](int start) { // Found valid combination of exactly k elements if (current.size() == k) { result.push_back(current); return; } // Pruning: not enough elements remaining to complete a combination // We have current.size() elements, need k total, and can pick from start..n if (current.size() + (n - start + 1) < k) { return; } for (int i = start; i <= n; i++) { current.push_back(i); backtrack(i + 1); // Start from i+1 to enforce increasing order (no duplicates) current.pop_back(); } }; backtrack(1); return result;}// Example: combinations(4, 2) = {{1,2},{1,3},{1,4},{2,3},{2,4},{3,4}}
Problem: N-Queens - Place N queens on NxN board so none attack each other.Approach: Place queens row by row, check constraints before placing.Why row-by-row? Since no two queens can share a row, there is exactly one queen per row. This reduces the search space from C(n^2, n) positions to n^n positions (one column choice per row), and with constraint checking, most branches are pruned immediately.Diagonal indexing trick: For an NxN board, cells on the same ”/” diagonal have the same row + col value, and cells on the same "" diagonal have the same row - col value. We add n to row - col to keep indices non-negative.
vector<vector<string>> solveNQueens(int n) { vector<vector<string>> result; vector<string> board(n, string(n, '.')); // Track which columns and diagonals are under attack // cols[c] = true if column c has a queen // diag1[row-col+n] = true if "\" diagonal is occupied // diag2[row+col] = true if "/" diagonal is occupied vector<bool> cols(n), diag1(2 * n), diag2(2 * n); function<void(int)> backtrack = [&](int row) { if (row == n) { result.push_back(board); // All n queens placed successfully return; } for (int col = 0; col < n; col++) { if (cols[col] || diag1[row - col + n] || diag2[row + col]) { continue; // Position under attack -- constraint pruning } // Place queen and mark all three attack lines board[row][col] = 'Q'; cols[col] = diag1[row - col + n] = diag2[row + col] = true; backtrack(row + 1); // Remove queen (backtrack) -- restore all three attack lines board[row][col] = '.'; cols[col] = diag1[row - col + n] = diag2[row + col] = false; } }; backtrack(0); return result;}// For n=4: 2 solutions. For n=8: 92 solutions.
Backtracking can be slow (exponential). Pruning cuts branches early.Analogy: Think of exploring a maze. Without pruning, you explore every dead end fully before backtracking. With pruning, you notice “this corridor is a dead end” before walking all the way in. The earlier you prune, the more time you save. In contest problems, good pruning can turn a TLE into an AC on the same algorithm.
Rule of Thumb: Use recursion for trees/graphs, backtracking, and when natural. Convert to iteration if stack overflow is a concern.
Contest tip: On Codeforces, the default stack size is about 256 KB. With each recursive call using roughly 100-200 bytes of stack, you can safely recurse to depth ~10^4. For deeper recursion (graphs with 10^5+ nodes), either use iterative DFS or add #pragma comment(linker, "/STACK:1000000000") on Windows, or submit with a custom stack size on Linux-based judges.