Using Backtracking Pattern
1. Understanding Backtracking Framework
| Component | Description | Usage |
|---|---|---|
| State Space Tree | Represents all possible solutions in a tree structure where each node is a partial solution. | Used to visualize and explore all valid paths to a solution. |
| Choice Function | Defines the set of choices available at each node in the state space tree. | Guides the exploration by specifying valid moves from a given state. |
| Constraint Function | Checks whether a partial solution can lead to a valid complete solution. | Prunes branches that cannot lead to a solution early in the search process. |
| Solution Function | Verifies if a complete solution has been found. | Determines when the backtracking algorithm should stop or continue exploring. |
Example: N-Queens Problem
function solveNQueens(n) {
const board = Array(n).fill().map(() => Array(n).fill(0));
const result = [];
function isSafe(row, col) {
for (let i = 0; i < row; i++) {
if (board[i][col] === 1) return false;
if (col - (row - i) >= 0 && board[i][col - (row - i)] === 1) return false;
if (col + (row - i) < n && board[i][col + (row - i)] === 1) return false;
}
return true;
}
function backtrack(row) {
if (row === n) {
result.push(board.map(row => [...row]));
return;
}
for (let col = 0; col < n; col++) {
if (isSafe(row, col)) {
board[row][col] = 1;
backtrack(row + 1);
board[row][col] = 0;
}
}
}
backtrack(0);
return result;
}
Key Concepts
- Recursive Exploration: Backtracking uses recursion to explore all possible solutions.
- Pruning: Invalid paths are pruned early to reduce search space.
- State Management: Each recursive call maintains a partial solution state.
- Solution Validation: A complete solution is confirmed using a solution function.
2. Solving N-Queens Problem
The N-Queens problem involves placing N queens on an N×N chessboard such that no two queens threaten each other. This is a classic backtracking problem.
| Property | Description | Value |
|---|---|---|
| Board Size | Size of the chessboard (N×N) | N |
| Constraint | Queens must not share rows, columns, or diagonals | None |
Example: N-Queens Solution
function solveNQueens(n) {
const board = Array(n).fill().map(() => Array(n).fill(0));
const result = [];
function isSafe(row, col) {
for (let i = 0; i < row; i++) {
if (board[i][col] === 1) return false;
if (board[i][col - (row - i)] === 1) return false;
if (board[i][col + (row - i)] === 1) return false;
}
return true;
}
function backtrack(row) {
if (row === n) {
result.push(board.map(row => row.join("")));
return;
}
for (let col = 0; col < n; col++) {
if (isSafe(row, col)) {
board[row][col] = 1;
backtrack(row + 1);
board[row][col] = 0;
}
}
}
backtrack(0);
return result;
}
console.log(solveNQueens(4));
Solution Approach
- Use a recursive backtracking algorithm to explore all valid queen placements.
- At each row, try placing a queen in every column and validate its safety.
- If placement is valid, proceed to the next row; otherwise, backtrack and try another column.
- When all rows are filled, store the valid configuration.
3. Solving Sudoku Puzzle
Example: Basic Backtracking Algorithm for Sudoku
function solveSudoku(board) {
const emptyCell = findEmptyCell(board);
if (!emptyCell) return true; // Puzzle solved
const [row, col] = emptyCell;
for (let num = 1; num <= 9; num++) {
if (isValidPlacement(board, row, col, num)) {
board[row][col] = num;
if (solveSudoku(board)) return true;
board[row][col] = 0; // Backtrack
}
}
return false; // No valid number found
}
function findEmptyCell(board) {
for (let i = 0; i < 9; i++) {
for (let j = 0; j < 9; j++) {
if (board[i][j] === 0) return [i, j];
}
}
return null;
}
function isValidPlacement(board, row, col, num) {
// Check row
for (let i = 0; i < 9; i++) {
if (board[row][i] === 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;
}
| Component | Description | Notes |
|---|---|---|
| findEmptyCell | function(board) |
Finds next empty cell (value 0) in the board |
| isValidPlacement | function(board, row, col, num) |
Checks if placing num at (row, col) is valid |
| solveSudoku | function(board) |
Main recursive backtracking function |
4. Searching Words in Grid
Backtracking is commonly used to search for words in a 2D grid of characters, such as in word search puzzles. The algorithm explores all possible paths from each cell to find a given word.
| Property | Description | Example |
|---|---|---|
| grid | 2D array of characters representing the board | [['A','B','C'], ['D','E','F'], ['G','H','I']] |
| word | String to search for in the grid | "ABE" |
| directions | Array of possible moves (up, down, left, right) | [[0,1], [1,0], [0,-1], [-1,0]] |
Example: Word Search in Grid
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) return false;
if (board[row][col] !== word[index]) return false;
const temp = board[row][col];
board[row][col] = '#'; // Mark as visited
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; // Restore
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;
}
const board = [
['A', 'B', 'C'],
['D', 'E', 'F'],
['G', 'H', 'I']
];
console.log(exist(board, 'ABE')); // true
'#') to avoid reusing them in the current path, and restores the original character after backtracking.
5. Generating Valid IP Addresses
Example: Generate Valid IP Addresses from String
function restoreIpAddresses(s) {
const result = [];
function backtrack(start, path) {
if (path.length === 4 && start === s.length) {
result.push(path.join("."));
return;
}
if (path.length >= 4 || start >= s.length) return;
for (let i = 1; i <= 3 && start + i <= s.length; i++) {
const segment = s.substring(start, start + i);
if (isValid(segment)) {
path.push(segment);
backtrack(start + i, path);
path.pop();
}
}
}
function isValid(segment) {
if (segment.length === 0 || segment.length > 3) return false;
if (segment[0] === "0" && segment.length > 1) return false;
const num = parseInt(segment);
return num >= 0 && num <= 255;
}
backtrack(0, []);
return result;
}
// Example usage:
console.log(restoreIpAddresses("25525511135"));
// Output: ["255.255.11.135", "255.255.111.35"]
| Property | Description | Constraints |
|---|---|---|
| Input String | String of digits (0-9) | Length between 1 and 12 |
| Valid IP Segment | Each part must be between 0 and 255 | No leading zeros (except for "0" itself) |
| Number of Segments | Must form exactly 4 segments | Each segment is a valid IP part |
6. Solving Combination Sum Problems
| Problem | Description | Solution Approach | Time Complexity |
|---|---|---|---|
| Combination Sum I | Find all unique combinations in candidates where the candidate numbers sum to target. | Backtracking with recursion, allowing reuse of elements. | O(N^(T/M)) where N is candidates size, T is target, M is min(candidates) |
| Combination Sum II | Each element can only be used once. Duplicates in candidates are handled. | Backtracking with sorting and skipping duplicates at each level. | O(2^N) |
| Combination Sum III | Find all valid combinations of k numbers that sum to n, using numbers 1-9 once each. | Backtracking with constraints on count and sum. | O(9^k) |
Example: Combination Sum I
function combinationSum(candidates, target) {
const result = [];
function backtrack(start, currentCombination, remainingTarget) {
if (remainingTarget === 0) {
result.push([...currentCombination]);
return;
}
for (let i = start; i < candidates.length; i++) {
if (candidates[i] <= remainingTarget) {
currentCombination.push(candidates[i]);
backtrack(i, currentCombination, remainingTarget - candidates[i]);
currentCombination.pop();
}
}
}
backtrack(0, [], target);
return result;
}
// Example usage:
const candidates = [2, 3, 6, 7];
const target = 7;
console.log(combinationSum(candidates, target));
// Output: [[2,2,3], [7]]
Example: Combination Sum II
function combinationSum2(candidates, target) {
candidates.sort((a, b) => a - b);
const result = [];
function backtrack(start, currentCombination, remainingTarget) {
if (remainingTarget === 0) {
result.push([...currentCombination]);
return;
}
for (let i = start; i < candidates.length; i++) {
if (i > start && candidates[i] === candidates[i - 1]) continue;
if (candidates[i] <= remainingTarget) {
currentCombination.push(candidates[i]);
backtrack(i + 1, currentCombination, remainingTarget - candidates[i]);
currentCombination.pop();
}
}
}
backtrack(0, [], target);
return result;
}
// Example usage:
const candidates = [10, 1, 2, 7, 6, 1, 5];
const target = 8;
console.log(combinationSum2(candidates, target));
// Output: [[1,1,6], [1,2,5], [1,7], [2,6]]
Example: Combination Sum III
function combinationSum3(k, n) {
const result = [];
function backtrack(start, currentCombination, remainingSum, remainingCount) {
if (remainingCount === 0 && remainingSum === 0) {
result.push([...currentCombination]);
return;
}
if (remainingCount === 0 || remainingSum <= 0) return;
for (let i = start; i <= 9; i++) {
if (i > remainingSum) break;
currentCombination.push(i);
backtrack(i + 1, currentCombination, remainingSum - i, remainingCount - 1);
currentCombination.pop();
}
}
backtrack(1, [], n, k);
return result;
}
// Example usage:
const k = 3;
const n = 7;
console.log(combinationSum3(k, n));
// Output: [[1,2,4]]
7. Solving Palindrome Partitioning
Palindrome Partitioning is a classic backtracking problem where we partition a string such that every substring is a palindrome.
| Property | Description | Example |
|---|---|---|
| Input | String to be partitioned | "aab" |
| Output | List of all possible palindrome partitions | [["a","a","b"], ["aa","b"]] |
Example: Palindrome Partitioning
function partition(s) {
const result = [];
function backtrack(start, path) {
if (start === s.length) {
result.push([...path]);
return;
}
for (let end = start; end < s.length; end++) {
const substring = s.slice(start, end + 1);
if (isPalindrome(substring)) {
path.push(substring);
backtrack(end + 1, path);
path.pop();
}
}
}
function isPalindrome(str) {
let left = 0;
let right = str.length - 1;
while (left < right) {
if (str[left] !== str[right]) return false;
left++;
right--;
}
return true;
}
backtrack(0, []);
return result;
}
console.log(partition("aab")); // [["a","a","b"], ["aa","b"]]
8. Generating Phone Letter Combinations
| Property | Description | Example |
|---|---|---|
| Input | String of digits (2-9) | "23" |
| Output | All possible letter combinations | ["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"] |
Example: Phone Letter Combinations
function letterCombinations(digits) {
if (digits.length === 0) return [];
const mapping = {
"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 = mapping[digits[index]];
for (let letter of letters) {
backtrack(index + 1, current + letter);
}
}
backtrack(0, "");
return result;
}
console.log(letterCombinations("23"));
9. Solving Rat in Maze Problem
The Rat in Maze problem is a classic backtracking challenge where we need to find a path from the top-left corner to the bottom-right corner of a maze, where 1 represents a valid path and 0 represents a blocked cell.
| Property | Description | Values |
|---|---|---|
| maze | 2D array representing the maze | [[1, 0, 1, 1], [1, 1, 1, 0], [0, 1, 1, 1]] |
| solution | 2D array storing the path | [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]] |
Example: Solving Rat in Maze
function solveMaze(maze) {
const n = maze.length;
const solution = Array(n).fill().map(() => Array(n).fill(0));
function isSafe(x, y) {
return x >= 0 && y >= 0 && x < n && y < n && maze[x][y] === 1;
}
function solveUtil(x, y) {
if (x === n - 1 && y === n - 1) {
solution[x][y] = 1;
return true;
}
if (isSafe(x, y)) {
solution[x][y] = 1;
if (solveUtil(x + 1, y)) return true;
if (solveUtil(x, y + 1)) return true;
solution[x][y] = 0;
return false;
}
return false;
}
if (solveUtil(0, 0)) {
return solution;
}
return null;
}
const maze = [
[1, 0, 1, 1],
[1, 1, 1, 0],
[0, 1, 1, 1],
[1, 1, 0, 1]
];
console.log(solveMaze(maze));
null.
10. Solving Knight's Tour Problem
The Knight's Tour problem involves finding a sequence of moves for a knight on a chessboard such that the knight visits every square exactly once. This is a classic application of backtracking.
| Property | Description | Notes |
|---|---|---|
| Board Size | Standard chessboard is 8x8, but can vary | Requires a grid of size n×n |
| Knight Moves | 8 possible moves from any position | Each move is 2 squares in one direction and 1 in perpendicular |
| Solution Type | Hamiltonian Path | Visits each square once, does not return to start |
Example: Knight's Tour Implementation
function solveKnightTour(n) {
const board = Array(n).fill().map(() => Array(n).fill(0));
const moves = [[2,1], [1,2], [-1,2], [-2,1], [-2,-1], [-1,-2], [1,-2], [2,-1]];
function isSafe(x, y) {
return x >= 0 && x < n && y >= 0 && y < n && board[x][y] === 0;
}
function solve(x, y, move) {
if (move === n * n) return true;
for (const [dx, dy] of moves) {
const newX = x + dx;
const newY = y + dy;
if (isSafe(newX, newY)) {
board[newX][newY] = move + 1;
if (solve(newX, newY, move + 1)) return true;
board[newX][newY] = 0;
}
}
return false;
}
board[0][0] = 1;
if (solve(0, 0, 1)) {
return board;
}
return null;
}
11. Solving Expression Add Operators
Solves the problem of inserting +, -, or * operators between digits of a string to form valid expressions that evaluate to a target value.
| Property | Description | Example |
|---|---|---|
| Input | String of digits and target integer | "123", 6 |
| Operators | Allowed operators: +, -, * |
None |
| Output | List of valid expressions that evaluate to target | ["1+2+3", "1*2*3"] |
Example: Expression Add Operators
function addOperators(num, target) {
const result = [];
function backtrack(start, path, value, prev) {
if (start === num.length) {
if (value === target) {
result.push(path);
}
return;
}
for (let i = start; i < num.length; i++) {
const current = num.substring(start, i + 1);
const currentNum = parseInt(current);
if (start === 0) {
backtrack(i + 1, current, currentNum, currentNum);
} else {
backtrack(i + 1, path + '+' + current, value + currentNum, currentNum);
backtrack(i + 1, path + '-' + current, value - currentNum, -currentNum);
backtrack(i + 1, path + '*' + current, value - prev + prev * currentNum, prev * currentNum);
}
if (current[0] === '0' && current.length > 1) break;
}
}
backtrack(0, '', 0, 0);
return result;
}
// Example usage:
console.log(addOperators("123", 6)); // ["1+2+3", "1*2*3"]
prev parameter helps handle operator precedence correctly for multiplication.
12. Restoring IP Addresses
Given a string containing only digits, return all possible valid IP address combinations that can be formed by inserting dots.
| Input | Output | Description |
|---|---|---|
| s = "25525511135" | ["255.255.11.135", "255.255.111.35"] |
Valid IP addresses formed from the string |
Example: Valid IP Address Combinations
function restoreIpAddresses(s) {
const result = [];
function backtrack(start, path) {
if (path.length === 4 && start === s.length) {
result.push(path.join('.'));
return;
}
if (path.length >= 4 || start >= s.length) return;
for (let i = 1; i <= 3; i++) {
const segment = s.substring(start, start + i);
if (isValid(segment)) {
path.push(segment);
backtrack(start + i, path);
path.pop();
}
}
}
function isValid(segment) {
if (segment.length === 0 || segment.length > 3) return false;
if (segment[0] === '0' && segment.length > 1) return false;
return parseInt(segment) <= 255;
}
backtrack(0, []);
return result;
}
// Example usage:
console.log(restoreIpAddresses("25525511135"));
// Output: ["255.255.11.135", "255.255.111.35"]