Backtracking Algorithm
Backtracking is a recursive algorithm used to solve problems by building a solution incrementally, one piece at a time, and removing solutions that fail to satisfy constraints of the problem.
It’s particularly useful for constraint satisfaction problems like puzzles, permutations, combinations, and finding paths.
Core Principles
The backtracking algorithm follows these core principles:
- Choice. At each step, you have a set of choices to make.
- Constraints. You have constraints that limit the choices you can make.
- Goal. Identify when a solution is found, allowing the function to backtrack from dead-ends.
Basic Template
Here’s a basic template of a backtracking algorithm in Javascript:
function backtrack(path, options) {
// Check if a complete solution is reached
if (isSolution(path)) {
processSolution(path);
return;
}
// Iterate over available choices
for (const choice of options) {
// Check if choice is valid under current constraints
if (isValid(path, choice)) {
path.push(choice); // Make a choice
backtrack(path, options); // Recursively explore with the choice made
path.pop(); // Undo choice (backtrack)
}
}
}
Example: N-Queens Problem
The N-Queens problem is a classic backtracking problem where you need to place N queens on an N×N chessboard so that no two queens attack each other.
Check if a Queen is Safe
To solve the N-Queens problem, you need to check if a queen can be safely placed at a given position (row, col)
on the board.
Here are the conditions to check:
- Row Check: Check if there is a queen in the same row on the left side.
- Upper Diagonal Check: Check if there is a queen in the upper diagonal on the left side.
- Lower Diagonal Check: Check if there is a queen in the lower diagonal on the left side.
If all these conditions are satisfied, the queen can be safely placed at (row, col)
.
function isSafe(board: number[][], row: number, col: number): boolean {
// Check row on left side
for (let j = 0; j < col; j++) {
if (board[row][j] === 1) {
return false;
}
}
// Check upper diagonal on left side
for (let i = row, j = col; i >= 0 && j >= 0; i--, j--) {
if (board[i][j] === 1) {
return false;
}
}
// Check lower diagonal on left side
for (let i = row, j = col; i < n && j >= 0; i++, j--) {
if (board[i][j] === 1) {
return false;
}
}
return true;
}
We don’t check the right side of the board because we are placing queens from left to right, so the right side is empty.
Backtracking Algorithm
The backtracking algorithm for the N-Queens problem involves recursively placing queens on the board and backtracking when a solution is not possible.
Here’s the high-level algorithm:
- Start with an empty board.
- Place a queen in the first column.
- Move to the next column and place a queen in a safe position.
- Repeat this process until all queens are placed or a solution is not possible.
- If a solution is not possible, backtrack to the previous column and try a different position.
const n = 4;
const board = Array.from({ length: n }, () => Array(n).fill(0));
function backtrack(board, col) {
// If the board is filled, return true
if (col >= n) {
return true;
}
for (let row = 0; row < n; row++) {
if (isSafe(board, row, col)) {
board[row][col] = 1;
// Recursively place queens in the next column
if (backtrack(board, col + 1)) {
return true;
}
// If placing queen at (row, col) didn't lead to a solution,
// backtrack by removing the queen
board[row][col] = 0;
}
}
return false;
}
if (!backtrack(board, 0)) {
console.log("No solution exists");
} else {
console.log(board);
}
Full Code
Here’s a Javascript implementation of the N-Queens problem:
function solveNQueens(n) {
const board = Array.from({ length: n }, () => Array(n).fill(0));
const result = [];
const isSafe = (board, row, col) => {
for (let j = 0; j < col; j++) {
if (board[row][j] === 1) {
return false;
}
}
for (let i = row, j = col; i >= 0 && j >= 0; i--, j--) {
if (board[i][j] === 1) {
return false;
}
}
for (let i = row, j = col; i < n && j >= 0; i++, j--) {
if (board[i][j] === 1) {
return false;
}
}
return true;
}
const backtrack = (board, col) => {
if (col >= n) {
result.push(board.map(row => row.join("")));
return;
}
for (let row = 0; row < n; row++) {
if (isSafe(board, row, col)) {
board[row][col] = 1;
backtrack(board, col + 1);
board[row][col] = 0;
}
}
}
backtrack(board, 0);
return result;
}
const n = 4;
const solutions = solveNQueens(n);
console.log(solutions);
Applications
Backtracking is used in various problems, including:
- Puzzles. Sudoku, crosswords, and word search puzzles.
- Permutations. Generating all permutations of a set of elements.
- Combinations. Generating all combinations of a set of elements.
- Graph Problems. Finding paths, cycles, or spanning trees in a graph.
- Constraint Satisfaction Problems. Scheduling, planning, and optimization problems.
- Game Playing. Chess, checkers, and other board games.
- Subset Problems. Finding subsets of a set that satisfy certain conditions.
Complexity
The time complexity of backtracking algorithms can vary depending on the problem.
In the worst-case scenario, the time complexity is exponential, O(b^d), where b is the branching factor and d is the depth of the recursion.