Backtracking is a algorithmic technique for solving problems recursively by trying to build a solution incrementally and abandoning solutions that fail to satisfy constraints.
What is Backtracking? #
Backtracking is a form of recursion where you:
- Make a choice
- Explore that path
- If it doesn’t work, undo the choice (backtrack)
- Try another option
Backtracking Template #
function backtrack(path, choices) {
// Base case
if (satisfiesCondition(path)) {
result.push([...path]);
return;
}
// Try each choice
for (const choice of choices) {
// Make choice
path.push(choice);
// Explore
backtrack(path, getNextChoices());
// Undo choice (backtrack)
path.pop();
}
}Classic Backtracking Problems #
Generate All Subsets #
function subsets(nums) {
const result = [];
function backtrack(start, current) {
result.push([...current]);
for (let i = start; i < nums.length; i++) {
current.push(nums[i]);
backtrack(i + 1, current);
current.pop();
}
}
backtrack(0, []);
return result;
}
console.log(subsets([1, 2, 3]));
// [[], [1], [1,2], [1,2,3], [1,3], [2], [2,3], [3]]
Generate All Permutations #
function permute(nums) {
const result = [];
function backtrack(current, remaining) {
if (remaining.length === 0) {
result.push([...current]);
return;
}
for (let i = 0; i < remaining.length; i++) {
current.push(remaining[i]);
const next = remaining.slice(0, i).concat(remaining.slice(i + 1));
backtrack(current, next);
current.pop();
}
}
backtrack([], nums);
return result;
}
console.log(permute([1, 2, 3]));
// [[1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1]]
Combination Sum #
function combinationSum(candidates, target) {
const result = [];
function backtrack(start, current, sum) {
if (sum === target) {
result.push([...current]);
return;
}
if (sum > target) return;
for (let i = start; i < candidates.length; i++) {
current.push(candidates[i]);
backtrack(i, current, sum + candidates[i]);
current.pop();
}
}
backtrack(0, [], 0);
return result;
}
console.log(combinationSum([2, 3, 6, 7], 7));
// [[2,2,3], [7]]
Letter Combinations of Phone Number #
function letterCombinations(digits) {
if (!digits) return [];
const phone = {
'2': 'abc', '3': 'def', '4': 'ghi', '5': 'jkl',
'6': 'mno', '7': 'pqrs', '8': 'tuv', '9': 'wxyz'
};
const result = [];
function backtrack(index, current) {
if (index === digits.length) {
result.push(current);
return;
}
const letters = phone[digits[index]];
for (const letter of letters) {
backtrack(index + 1, current + letter);
}
}
backtrack(0, '');
return result;
}
console.log(letterCombinations("23"));
// ["ad","ae","af","bd","be","bf","cd","ce","cf"]
N-Queens #
function solveNQueens(n) {
const result = [];
const board = Array(n).fill(0).map(() => Array(n).fill('.'));
function isValid(row, col) {
// Check column
for (let i = 0; i < row; i++) {
if (board[i][col] === 'Q') return false;
}
// Check diagonal
for (let i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) {
if (board[i][j] === 'Q') return false;
}
// Check anti-diagonal
for (let i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) {
if (board[i][j] === 'Q') return false;
}
return true;
}
function backtrack(row) {
if (row === n) {
result.push(board.map(r => r.join('')));
return;
}
for (let col = 0; col < n; col++) {
if (isValid(row, col)) {
board[row][col] = 'Q';
backtrack(row + 1);
board[row][col] = '.';
}
}
}
backtrack(0);
return result;
}
console.log(solveNQueens(4));Sudoku Solver #
function solveSudoku(board) {
function isValid(row, col, num) {
// Check row
for (let j = 0; j < 9; j++) {
if (board[row][j] === num) return false;
}
// Check column
for (let i = 0; i < 9; i++) {
if (board[i][col] === num) return false;
}
// Check 3x3 box
const boxRow = Math.floor(row / 3) * 3;
const boxCol = Math.floor(col / 3) * 3;
for (let i = 0; i < 3; i++) {
for (let j = 0; j < 3; j++) {
if (board[boxRow + i][boxCol + j] === num) return false;
}
}
return true;
}
function backtrack() {
for (let row = 0; row < 9; row++) {
for (let col = 0; col < 9; col++) {
if (board[row][col] === '.') {
for (let num = 1; num <= 9; num++) {
const char = num.toString();
if (isValid(row, col, char)) {
board[row][col] = char;
if (backtrack()) return true;
board[row][col] = '.';
}
}
return false;
}
}
}
return true;
}
backtrack();
return board;
}Word Search #
function exist(board, word) {
const rows = board.length;
const cols = board[0].length;
function backtrack(row, col, index) {
if (index === word.length) return true;
if (row < 0 || row >= rows || col < 0 || col >= cols ||
board[row][col] !== word[index]) {
return false;
}
const temp = board[row][col];
board[row][col] = '#';
const found = backtrack(row + 1, col, index + 1) ||
backtrack(row - 1, col, index + 1) ||
backtrack(row, col + 1, index + 1) ||
backtrack(row, col - 1, index + 1);
board[row][col] = temp;
return found;
}
for (let i = 0; i < rows; i++) {
for (let j = 0; j < cols; j++) {
if (backtrack(i, j, 0)) return true;
}
}
return false;
}Palindrome Partitioning #
function partition(s) {
const result = [];
function isPalindrome(str) {
let left = 0, right = str.length - 1;
while (left < right) {
if (str[left] !== str[right]) return false;
left++;
right--;
}
return true;
}
function backtrack(start, current) {
if (start === s.length) {
result.push([...current]);
return;
}
for (let end = start + 1; end <= s.length; end++) {
const substring = s.slice(start, end);
if (isPalindrome(substring)) {
current.push(substring);
backtrack(end, current);
current.pop();
}
}
}
backtrack(0, []);
return result;
}
console.log(partition("aab"));
// [["a","a","b"], ["aa","b"]]
Generate Parentheses #
function generateParenthesis(n) {
const result = [];
function backtrack(current, open, close) {
if (current.length === 2 * n) {
result.push(current);
return;
}
if (open < n) {
backtrack(current + '(', open + 1, close);
}
if (close < open) {
backtrack(current + ')', open, close + 1);
}
}
backtrack('', 0, 0);
return result;
}
console.log(generateParenthesis(3));
// ["((()))","(()())","(())()","()(())","()()()"]
Restore IP Addresses #
function restoreIpAddresses(s) {
const result = [];
function isValid(segment) {
if (segment.length > 3) return false;
if (segment[0] === '0' && segment.length > 1) return false;
return parseInt(segment) <= 255;
}
function backtrack(start, current) {
if (current.length === 4) {
if (start === s.length) {
result.push(current.join('.'));
}
return;
}
for (let len = 1; len <= 3; len++) {
if (start + len > s.length) break;
const segment = s.slice(start, start + len);
if (isValid(segment)) {
current.push(segment);
backtrack(start + len, current);
current.pop();
}
}
}
backtrack(0, []);
return result;
}
console.log(restoreIpAddresses("25525511135"));
// ["255.255.11.135","255.255.111.35"]
Subsets II (With Duplicates) #
function subsetsWithDup(nums) {
nums.sort((a, b) => a - b);
const result = [];
function backtrack(start, current) {
result.push([...current]);
for (let i = start; i < nums.length; i++) {
if (i > start && nums[i] === nums[i - 1]) continue;
current.push(nums[i]);
backtrack(i + 1, current);
current.pop();
}
}
backtrack(0, []);
return result;
}
console.log(subsetsWithDup([1, 2, 2]));
// [[],[1],[1,2],[1,2,2],[2],[2,2]]
Permutations II (With Duplicates) #
function permuteUnique(nums) {
nums.sort((a, b) => a - b);
const result = [];
const used = new Array(nums.length).fill(false);
function backtrack(current) {
if (current.length === nums.length) {
result.push([...current]);
return;
}
for (let i = 0; i < nums.length; i++) {
if (used[i]) continue;
if (i > 0 && nums[i] === nums[i - 1] && !used[i - 1]) continue;
current.push(nums[i]);
used[i] = true;
backtrack(current);
current.pop();
used[i] = false;
}
}
backtrack([]);
return result;
}Combination Sum III #
function combinationSum3(k, n) {
const result = [];
function backtrack(start, current, sum) {
if (current.length === k && sum === n) {
result.push([...current]);
return;
}
if (current.length >= k || sum >= n) return;
for (let i = start; i <= 9; i++) {
current.push(i);
backtrack(i + 1, current, sum + i);
current.pop();
}
}
backtrack(1, [], 0);
return result;
}
console.log(combinationSum3(3, 9));
// [[1,2,6],[1,3,5],[2,3,4]]
Optimization Techniques #
Pruning #
Stop exploring paths that can’t lead to solution:
function combinationSum(candidates, target) {
candidates.sort((a, b) => a - b);
const result = [];
function backtrack(start, current, sum) {
if (sum === target) {
result.push([...current]);
return;
}
for (let i = start; i < candidates.length; i++) {
// Pruning: skip if sum exceeds target
if (sum + candidates[i] > target) break;
current.push(candidates[i]);
backtrack(i, current, sum + candidates[i]);
current.pop();
}
}
backtrack(0, [], 0);
return result;
}Memoization #
Cache results for repeated subproblems:
function wordBreak(s, wordDict) {
const memo = new Map();
const wordSet = new Set(wordDict);
function backtrack(start) {
if (start === s.length) return true;
if (memo.has(start)) return memo.get(start);
for (let end = start + 1; end <= s.length; end++) {
const word = s.slice(start, end);
if (wordSet.has(word) && backtrack(end)) {
memo.set(start, true);
return true;
}
}
memo.set(start, false);
return false;
}
return backtrack(0);
}When to Use Backtracking #
- Constraint satisfaction - Sudoku, N-Queens
- Combinatorial - Subsets, permutations
- Path finding - Word search, maze solving
- Optimization - Maximize/minimize with constraints
Time Complexity #
Backtracking is often exponential:
| Problem | Time Complexity |
|---|---|
| Subsets | O(2^n) |
| Permutations | O(n!) |
| Combination Sum | O(2^n) |
| N-Queens | O(n!) |
Tips for Backtracking #
- Define base case - When to add result
- Make choice - Try each option
- Explore - Recursive call
- Undo choice - Backtrack
- Prune early - Skip invalid paths
- Use constraints - Reduce search space
- Visualize - Draw decision tree
Backtracking is powerful for exploring all possible solutions. Master these patterns to solve complex combinatorial problems efficiently.