- Home
- /
- Tutorials
- /
- DSA Tutorial
- /
- Classic Backtracking Problems
Backtracking
Classic Backtracking Problems
These three cover the shapes: placing pieces under constraints, searching a grid, and filling in blanks. Everything else is a variation.
N-Queens - placing under constraints
Place n queens on an n×n board so none attack each other. One queen per row, so the only question is which column.
N-Queens
javascript
function solveNQueens(n) {
const results = []
const columns = new Set()
const diagonal = new Set() // row - col
const antiDiagonal = new Set() // row + col
const placement = []
function place(row) {
if (row === n) {
results.push([...placement])
return
}
for (let col = 0; col < n; col++) {
// O(1) conflict check instead of scanning the board.
if (columns.has(col)) continue
if (diagonal.has(row - col)) continue
if (antiDiagonal.has(row + col)) continue
columns.add(col)
diagonal.add(row - col)
antiDiagonal.add(row + col)
placement.push(col)
place(row + 1)
placement.pop()
antiDiagonal.delete(row + col)
diagonal.delete(row - col)
columns.delete(col)
}
}
place(0)
return results
}
console.log(solveNQueens(8).length) // 92The three sets are the trick. row - col is constant along one diagonal and row + col along the other, so a conflict check is a hash lookup rather than a board scan.
Word search - backtracking on a grid
Find a word in a grid
javascript
function exist(board, word) {
const rows = board.length
const cols = board[0].length
function search(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
// Mark as used so this path cannot reuse the same cell.
const original = board[row][col]
board[row][col] = "#"
const found =
search(row + 1, col, index + 1) ||
search(row - 1, col, index + 1) ||
search(row, col + 1, index + 1) ||
search(row, col - 1, index + 1)
board[row][col] = original // restore on the way out
return found
}
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
if (search(r, c, 0)) return true
}
}
return false
}
const demoGrid = [["c", "a"], ["b", "t"]]
console.log(exist(demoGrid, "cat")) // true
console.log(exist(demoGrid, "cbt")) // falseOverwriting the cell and restoring it is the visited set - no extra structure needed. Forgetting to restore is the classic bug, and it produces false negatives that are hard to spot.
Sudoku - filling blanks
Sudoku solver
javascript
function solveSudoku(board) {
function isValid(row, col, ch) {
const boxRow = 3 * Math.floor(row / 3)
const boxCol = 3 * Math.floor(col / 3)
for (let i = 0; i < 9; i++) {
if (board[row][i] === ch) return false
if (board[i][col] === ch) return false
if (board[boxRow + Math.floor(i / 3)][boxCol + (i % 3)] === ch) return false
}
return true
}
function solve() {
for (let row = 0; row < 9; row++) {
for (let col = 0; col < 9; col++) {
if (board[row][col] !== ".") continue
for (let d = 1; d <= 9; d++) {
const ch = String(d)
if (!isValid(row, col, ch)) continue
board[row][col] = ch
if (solve()) return true
board[row][col] = "." // undo
}
return false // no digit fits — this branch is dead
}
}
return true // no blanks left
}
solve()
return board
}
console.log("solveSudoku fills the board in place and returns nothing")The return false after the digit loop is essential. It says "nothing works here", which forces the caller to undo its own choice - that is the backtrack.
The common thread
- State is changed on the way down and restored on the way up.
- Invalid branches are abandoned as early as possible.
- A conflict check should be O(1) - use sets, not scans.
- The base case returns; every other path must undo before it does.
Avoiding duplicate results
When the input contains repeats, the naive recursion produces the same combination several times. The fix is to sort first and then skip a value that equals its predecessor at the same depth - not globally, only among siblings in the tree.
Subsets with duplicates
javascript
function subsetsWithDup(nums) {
const sorted = [...nums].sort((a, b) => a - b)
const results = []
const path = []
function backtrack(start) {
results.push([...path])
for (let i = start; i < sorted.length; i++) {
// Skip a repeat only when it is a sibling, not when nested.
if (i > start && sorted[i] === sorted[i - 1]) continue
path.push(sorted[i])
backtrack(i + 1)
path.pop()
}
}
backtrack(0)
return results
}
console.log(subsetsWithDup([1, 2, 2]).length) // 6, not 8The condition is i > start, not i > 0. The first is "this is a sibling of a value I already tried at this level"; the second would wrongly block legitimate nesting like [2, 2].
The recurring set
- Subsets - include or exclude each element.
- Permutations - track which indices are used.
- Combination sum - recurse on the same index when reuse is allowed.
- N-Queens - validity check over columns and both diagonals.
- Word search - mark the grid cell, recurse, unmark it.
