Backtracking


Overview

Backtracking is depth-first search over a decision tree. Each recursive call represents a partial solution: make one choice, recurse, then undo that choice so the next branch starts from the same state. When recording a solution, append a copy of the current path or board because the original will continue to change.

A 1D problem builds a sequence of choices from an array, string, or list of groups. The recursive state is usually an index such as start plus a mutable curr path. Recurse with i + 1 when each input position may be used once, with i when reuse is allowed, or over every unused value for permutations. Sorting can expose duplicates and allow early termination.

A 2D problem searches or fills a board. The state includes a row and column, or a single cell index converted into coordinates. Mark a cell or update row, column, and region constraints before recursing, then restore them if the branch fails. A path search usually recurses to neighboring cells, while a board-filling problem advances to the next row or cell.

Backtracking is usually exponential because the search tree may contain many combinations. Effective validity checks, duplicate skipping, and early pruning reduce the number of branches explored. Recursion uses space proportional to the maximum decision depth, in addition to any constraint-tracking structures and returned solutions.

Templates

1D backtracking

  • Keep the current partial solution in curr and use start to describe which choices remain.
  • When curr is complete, append a copy and return.
  • Iterate over the available choices, skipping invalid or duplicate choices.
  • Append the choice, recurse with the correct next index, and pop it afterward.
import java.util.ArrayList;
import java.util.List;

public class OneDimensionalBacktrackingTemplate {
	public List<List<Integer>> combinations(int[] values, int k) {
		List<List<Integer>> res = new ArrayList<>();
		List<Integer> curr = new ArrayList<>();

		backTrack(values, k, 0, curr, res);
		return res;
	}

	private void backTrack(int[] values, int k, int start, List<Integer> curr, List<List<Integer>> res) {
		if (curr.size() == k) {
			res.add(new ArrayList<>(curr));
			return;
		}

		for (int i = start; i < values.length; i++) {
			curr.add(values[i]);
			backTrack(values, k, i + 1, curr, res);
			curr.remove(curr.size() - 1);
		}
	}
}

2D backtracking

  • Convert the current cell index into row and col, and skip cells that are already filled.
  • If every cell has been processed, return true.
  • Try each valid choice and update the board and any constraint-tracking state.
  • Recurse to the next cell; if the branch fails, restore the cell before trying another choice.
  • For a neighbor-based path search, recurse to valid neighboring coordinates instead of cell + 1.
public class TwoDimensionalBacktrackingTemplate {
	private int R;
	private int C;

	public boolean solve(int[][] board) {
		R = board.length;
		C = board[0].length;
		return backTrack(board, 0);
	}

	private boolean backTrack(int[][] board, int cell) {
		if (cell == R * C) {
			return true;
		}

		int row = cell / C;
		int col = cell % C;
		if (board[row][col] != 0) {
			return backTrack(board, cell + 1);
		}

		for (int choice = 1; choice <= 9; choice++) {
			if (!isValid(board, row, col, choice)) {
				continue;
			}

			board[row][col] = choice;
			if (backTrack(board, cell + 1)) {
				return true;
			}
			board[row][col] = 0;
		}
		return false;
	}

	private boolean isValid(int[][] board, int row, int col, int choice) {
		// Replace with the problem's row, column, and region checks.
		return true;
	}
}

Problems

78. Subsets

Generate the power set with a binary choice at every index: skip the current number or include it. Record a copy of curr after all N choices have been made, and pop an included number when backtracking.

import java.util.ArrayList;
import java.util.List;

class Solution {
	public List<List<Integer>> subsets(int[] nums) {
		List<List<Integer>> res = new ArrayList<>();
		int N = nums.length;

		backTrack(nums, 0, N, new ArrayList<>(), res);
		return res;
	}

	private void backTrack(int[] nums, int i, int N, List<Integer> curr, List<List<Integer>> res) {
		if (i == N) {
			res.add(new ArrayList<>(curr));
			return;
		}

		int num = nums[i];
		// do not take current number
		backTrack(nums, i + 1, N, curr, res);
		// take current number
		curr.add(num);
		backTrack(nums, i + 1, N, curr, res);
		curr.remove(curr.size() - 1);
	}
}

46. Permutations

Generate every ordering of the distinct values. At each depth, choose a number not in seen, append it to curr, recurse, then remove it from both structures. Creating each length-N result gives O(N × N!) time and O(N) auxiliary space.

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

class Solution {
	public List<List<Integer>> permute(int[] nums) {
		// Time complexity: O(N * N!)
		// Space complexity: O(N)
		List<List<Integer>> res = new ArrayList<>();
		List<Integer> curr = new ArrayList<>();
		Set<Integer> seen = new HashSet<>();
		int N = nums.length;

		backTrack(nums, 0, N, curr, seen, res);
		return res;
	}

	private void backTrack(int[] nums, int count, int N, List<Integer> curr, Set<Integer> seen, List<List<Integer>> res) {
		if (count == N) {
			res.add(new ArrayList<>(curr));
			return;
		}

		for (int i = 0; i < N; i++) {
			if (!seen.contains(nums[i])) {
				seen.add(nums[i]);
				curr.add(nums[i]);
				backTrack(nums, count + 1, N, curr, seen, res);
				seen.remove(nums[i]);
				curr.remove(curr.size() - 1);
			}
		}
	}
}

39. Combination Sum

Choose numbers that total the target, with unlimited reuse. Sort candidates and track a running total; stop the loop when adding the next number would exceed the target. Recurse with the same index so the current number may be selected again.

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

class Solution {
	public List<List<Integer>> combinationSum(int[] candidates, int target) {
		Arrays.sort(candidates);
		List<List<Integer>> res = new ArrayList<>();
		List<Integer> curr = new ArrayList<>();

		backTrack(candidates, target, 0, 0, curr, res);
		return res;
	}

	private void backTrack(int[] candidates, int target, int start, int total, List<Integer> curr, List<List<Integer>> res) {
		if (total == target) {
			res.add(new ArrayList<>(curr));
			return;
		}

		for (int i = start; i < candidates.length; i++) {
			int num = candidates[i];
			if (total + num > target) {
				break;
			}

			curr.add(num);
			total += num;
			backTrack(candidates, target, i, total, curr, res);
			curr.remove(curr.size() - 1);
			total -= num;
		}
	}
}

40. Combination Sum II

Choose numbers that total the target, using each candidate at most once. Sort candidates so the search can stop when a number exceeds remaining, and skip equal candidates at the same recursion level to avoid duplicate combinations.

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

class Solution {
	public List<List<Integer>> combinationSum2(int[] candidates, int target) {
		int N = candidates.length;
		List<List<Integer>> res = new ArrayList<>();
		List<Integer> curr = new ArrayList<>();
		Arrays.sort(candidates);

		backTrack(candidates, N, 0, target, curr, res);
		return res;
	}

	private void backTrack(int[] candidates, int N, int start, int remaining, List<Integer> curr, List<List<Integer>> res) {
		if (remaining == 0) {
			res.add(new ArrayList<>(curr));
			return;
		}

		for (int i = start; i < N; i++) {
			if (i > start && candidates[i] == candidates[i - 1]) {
				continue;
			}

			if (remaining - candidates[i] < 0) {
				break;
			}

			curr.add(candidates[i]);
			backTrack(
				candidates,
				N,
				i + 1,
				remaining - candidates[i],
				curr,
				res
			);
			curr.remove(curr.size() - 1);
		}
	}
}

22. Generate Parentheses

Generate every valid sequence of n parenthesis pairs. Track how many left and right parentheses have been placed, and prune whenever either count exceeds n or right parentheses outnumber left parentheses. Otherwise, try appending each character and restore curr afterward.

import java.util.ArrayList;
import java.util.List;

class Solution {
	public List<String> generateParenthesis(int n) {
		List<String> res = new ArrayList<>();
		StringBuilder curr = new StringBuilder();

		backTrack(n, 0, 0, curr, res);
		return res;
	}

	private void backTrack(int n, int left, int right, StringBuilder curr, List<String> res) {
		if (left > n || right > n || right > left) {
			return;
		}

		if (left == n && right == n) {
			res.add(curr.toString());
			return;
		}

		curr.append('(');
		backTrack(n, left + 1, right, curr, res);
		curr.deleteCharAt(curr.length() - 1);
		curr.append(')');
		backTrack(n, left, right + 1, curr, res);
		curr.deleteCharAt(curr.length() - 1);
	}
}

Find whether a word can be formed by adjacent cells without reusing a cell. Start DFS from each matching position, temporarily mark the current cell, and restore it while backtracking.

class Solution {
	private int R;
	private int C;
	private int N;
	private char[][] board;
	private String word;

	public boolean exist(char[][] board, String word) {
		this.R = board.length;
		this.C = board[0].length;
		this.N = word.length();
		this.board = board;
		this.word = word;

		for (int r = 0; r < R; r++) {
			for (int c = 0; c < C; c++) {
				if (backTrack(r, c, 0)) {
					return true;
				}
			}
		}

		return false;
	}

	private boolean backTrack(int r, int c, int idx) {
		if (r < 0 || r >= R ||
			c < 0 || c >= C ||
			idx >= N ||
			board[r][c] != word.charAt(idx)) {
			return false;
		}

		if (idx == N - 1) {
			return true;
		}

		board[r][c] = '*';
		boolean left = backTrack(r, c - 1, idx + 1);
		boolean right = backTrack(r, c + 1, idx + 1);
		boolean top = backTrack(r - 1, c, idx + 1);
		boolean bottom = backTrack(r + 1, c, idx + 1);
		board[r][c] = word.charAt(idx);

		return left || right || top || bottom;
	}
}

131. Palindrome Partitioning

Precompute is_pal[left][right] for every substring so backtracking can test each partition choice in constant time. From start, try every palindromic prefix, append it to curr, recurse from its ending position, and pop it afterward.

import java.util.ArrayList;
import java.util.List;

class Solution {
	public List<List<String>> partition(String s) {
		int N = s.length();
		List<List<String>> res = new ArrayList<>();
		List<String> curr = new ArrayList<>();
		boolean[][] isPal = new boolean[N][N];

		for (int length = 1; length <= N; length++) {
			for (int left = 0; left < N - length + 1; left++) {
				int right = length + left - 1;

				if (s.charAt(left) == s.charAt(right) &&
					(length <= 2 || isPal[left + 1][right - 1])) {
					isPal[left][right] = true;
				}
			}
		}

		backTrack(s, 0, N, isPal, curr, res);
		return res;
	}

	private void backTrack(String s, int start, int N, boolean[][] isPal, List<String> curr, List<List<String>> res) {
		if (start == N) {
			res.add(new ArrayList<>(curr));
			return;
		}

		for (int end = start + 1; end <= N; end++) {
			String sCur = s.substring(start, end);
			if (isPal[start][end - 1]) {
				curr.add(sCur);
				backTrack(s, end, N, isPal, curr, res);
				curr.remove(curr.size() - 1);
			}
		}
	}
}

1087. Brace Expansion

Parse the expression into ordered groups of choices. A normal character contributes one choice, while a brace group contributes its sorted options. Backtrack through one group at a time and append each completed string to res.

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

class Solution {
	public String[] expand(String s) {
		List<List<Character>> groups = new ArrayList<>();
		int i = 0;

		while (i < s.length()) {
			if (s.charAt(i) == '{') {
				List<Character> options = new ArrayList<>();
				i += 1;
				while (s.charAt(i) != '}') {
					if (s.charAt(i) != ',') {
						options.add(s.charAt(i));
					}
					i += 1;
				}
				i += 1;
				Collections.sort(options);
				groups.add(options);
			} else {
				groups.add(List.of(s.charAt(i)));
				i += 1;
			}
		}

		List<String> res = new ArrayList<>();
		StringBuilder curr = new StringBuilder();
		int N = groups.size();

		backTrack(groups, N, 0, curr, res);
		return res.toArray(new String[0]);
	}

	private void backTrack(List<List<Character>> groups, int N, int idx, StringBuilder curr, List<String> res) {
		if (idx == N) {
			res.add(curr.toString());
			return;
		}

		for (char c : groups.get(idx)) {
			curr.append(c);
			backTrack(groups, N, idx + 1, curr, res);
			curr.deleteCharAt(curr.length() - 1);
		}
	}
}

51. N-Queens

Place exactly one queen in each row. Track occupied columns, descending diagonals with row - col + n - 1, and ascending diagonals with row + col. After placing a queen, mark all three constraints, recurse to the next row, then remove the queen and restore the status arrays.

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

class Solution {
	private int n;
	private List<List<String>> res;
	private List<int[]> currQueens;
	private boolean[] colStatus;
	private boolean[] diag1Status;
	private boolean[] diag2Status;

	public List<List<String>> solveNQueens(int n) {
		this.n = n;
		res = new ArrayList<>();
		currQueens = new ArrayList<>();
		colStatus = new boolean[n];
		diag1Status = new boolean[2 * n - 1];
		diag2Status = new boolean[2 * n - 1];

		backTrack(0);
		return res;
	}

	private void backTrack(int row) {
		if (row == n) {
			res.add(drawBoard(currQueens));
			return;
		}

		for (int col = 0; col < n; col++) {
			if (colStatus[col] == false &&
				diag1Status[row - col + n - 1] == false &&
				diag2Status[row + col] == false) {
				currQueens.add(new int[] {row, col});
				colStatus[col] = true;
				diag1Status[row - col + n - 1] = true;
				diag2Status[row + col] = true;
				backTrack(row + 1);
				currQueens.remove(currQueens.size() - 1);
				colStatus[col] = false;
				diag1Status[row - col + n - 1] = false;
				diag2Status[row + col] = false;
			}
		}
	}

	private List<String> drawBoard(List<int[]> queens) {
		int N = queens.size();
		char[][] board = new char[N][N];
		for (char[] row : board) {
			Arrays.fill(row, '.');
		}
		for (int[] queen : queens) {
			int r = queen[0];
			int c = queen[1];
			board[r][c] = 'Q';
		}

		List<String> result = new ArrayList<>();
		for (char[] row : board) {
			result.add(new String(row));
		}
		return result;
	}
}

37. Sudoku Solver

Preprocess the board into row, column, and box occupancy tables. Traverse cells from left to right and top to bottom, skipping filled cells. For an empty cell, try each valid digit, update all three tables, recurse, and restore the cell and tables if that choice cannot complete the board.

class Solution {
	public void solveSudoku(char[][] board) {
		int[][] rows = new int[9][10];
		int[][] cols = new int[9][10];
		int[][] boxs = new int[9][10];

		// Pre-processing
		for (int r = 0; r < 9; r++) {
			for (int c = 0; c < 9; c++) {
				char ch = board[r][c];
				if (ch != '.') {
					int n = ch - '0';
					int bx = c / 3;
					int by = r / 3;
					rows[r][n] = 1;
					cols[c][n] = 1;
					boxs[by * 3 + bx][n] = 1;
				}
			}
		}

		fillBoard(board, 0, 0, rows, cols, boxs);
	}

	private boolean fillBoard(char[][] board, int x, int y, int[][] rows, int[][] cols, int[][] boxs) {
		if (y == 9) {
			return true;
		}

		// Compute next coordinates
		int nx = (x + 1) % 9;
		int ny = nx == 0 ? y + 1 : y;

		// Filled at current location, go to next location
		if (board[y][x] != '.') {
			return fillBoard(board, nx, ny, rows, cols, boxs);
		}

		// Try 1-9 one by one
		for (int i = 1; i < 10; i++) {
			int boxKey = (y / 3) * 3 + x / 3;
			if (rows[y][i] == 0 &&
				cols[x][i] == 0 &&
				boxs[boxKey][i] == 0) {
				rows[y][i] = 1;
				cols[x][i] = 1;
				boxs[boxKey][i] = 1;
				board[y][x] = (char) ('0' + i);
				if (fillBoard(board, nx, ny, rows, cols, boxs)) {
					return true;
				}
				rows[y][i] = 0;
				cols[x][i] = 0;
				boxs[boxKey][i] = 0;
				board[y][x] = '.';
			}
		}
		return false;
	}
}