Skip to main content

Backtracking on Grids and Constraints

Editorial

The choices live on a board

The same choose, explore, unchoose, with two changes. The choices are places on a board: the next cell of a path, the square for a queen, the digit for a Sudoku cell. And every choice has to keep the board's rules. Check each one the moment it's made: a choice that breaks a rule ends its whole branch at once, before anything is built on it. On an 8 × 8 board the queens search makes 2,057 calls that way; placing a queen in every row and checking only full boards takes 19,173,961.

Four 4 × 4 chequered boards in a row, joined by arrows. Board 1, row 0: a purple queen in column 0, with a pill reading column 0 free. Board 2, row 1: its first two squares crossed in red, the queen in column 2, pill 0, 1 attacked. Board 3, row 2, edged in red: all four squares crossed, pill all attacked. Board 4, back to row 1: the row 1 queen moved from column 2, where a dashed ring is left, to column 3, and row 2 has a queen in column 1 with its other three squares crossed, pill column 3, then 1. Below, a ticked card reads checked as placed, 2,057 calls at n = 8, 92 boards found; a crossed card reads checked at the end, 19,173,961 calls at n = 8, the same 92 boards. Focal line: a broken rule cuts its whole branch at once. Title: check each queen as it's placed.

Walk a grid: mark the cell, unmark it after

Word Search asks if a word can be traced on a grid of letters through neighbouring cells, each cell used at most once. The path is the choice list. From a matching cell, try its four neighbours for the next letter; mark the cell while the path is on it, and put its letter back on the way out, so a different path can still use it:

Two copies of the lesson's 3 × 4 grid of letters, rows ABCE, SFCS and ADEE. On the left, in a zone titled word ABCCED, the cells A, B, C, the C below it, the E below that and the D to its left are solid purple, joined by arrows in that order, and the other cells are pale; a ticked card under it reads ABCCED: true, 6 cells, each used once. On the right, in a zone titled word ABCB, only A, B and C along the top row are purple; a dashed red arrow arcs from the C back to the B, which is ringed in red, with a cross on the arc and a pill reading on the path; a crossed card under it reads ABCB: false, its only B is on the path. Focal line: mark on the way in, restore on the way out. Title: the path marks its cells.
bool found(vector<vector<char>>& board, const string& word, int r, int c, int k) {
    if (k == (int)word.size()) return true;                        // every letter matched
    if (r < 0 || c < 0 || r >= (int)board.size() || c >= (int)board[0].size() || board[r][c] != word[k])
        return false;
    char keep = board[r][c];
    board[r][c] = '#';                                             // mark: this path is on the cell
    bool ok = found(board, word, r + 1, c, k + 1) || found(board, word, r - 1, c, k + 1)
           || found(board, word, r, c + 1, k + 1) || found(board, word, r, c - 1, k + 1);
    board[r][c] = keep;                                            // unmark on the way back
    return ok;
}

bool exist(vector<vector<char>>& board, const string& word) {
    for (int r = 0; r < (int)board.size(); r++)
        for (int c = 0; c < (int)board[0].size(); c++)
            if (found(board, word, r, c, 0)) return true;
    return false;
}
def exist(board, word):
    rows, cols = len(board), len(board[0])

    def found(r, c, k):
        if k == len(word):
            return True                          # every letter matched
        if not (0 <= r < rows and 0 <= c < cols) or board[r][c] != word[k]:
            return False
        keep, board[r][c] = board[r][c], "#"     # mark: this path is on the cell
        ok = (found(r + 1, c, k + 1) or found(r - 1, c, k + 1)
              or found(r, c + 1, k + 1) or found(r, c - 1, k + 1))
        board[r][c] = keep                       # unmark on the way back
        return ok

    return any(found(r, c, 0) for r in range(rows) for c in range(cols))
static boolean found(char[][] board, String word, int r, int c, int k) {
    if (k == word.length()) return true;                           // every letter matched
    if (r < 0 || c < 0 || r >= board.length || c >= board[0].length || board[r][c] != word.charAt(k))
        return false;
    char keep = board[r][c];
    board[r][c] = '#';                                             // mark: this path is on the cell
    boolean ok = found(board, word, r + 1, c, k + 1) || found(board, word, r - 1, c, k + 1)
              || found(board, word, r, c + 1, k + 1) || found(board, word, r, c - 1, k + 1);
    board[r][c] = keep;                                            // unmark on the way back
    return ok;
}

static boolean exist(char[][] board, String word) {
    for (int r = 0; r < board.length; r++)
        for (int c = 0; c < board[0].length; c++)
            if (found(board, word, r, c, 0)) return true;
    return false;
}
function exist(board, word) {
    const rows = board.length, cols = board[0].length;
    function found(r, c, k) {
        if (k === word.length) return true;                        // every letter matched
        if (r < 0 || c < 0 || r >= rows || c >= cols || board[r][c] !== word[k]) return false;
        const keep = board[r][c];
        board[r][c] = "#";                                         // mark: this path is on the cell
        const ok = found(r + 1, c, k + 1) || found(r - 1, c, k + 1)
                || found(r, c + 1, k + 1) || found(r, c - 1, k + 1);
        board[r][c] = keep;                                        // unmark on the way back
        return ok;
    }
    for (let r = 0; r < rows; r++)
        for (let c = 0; c < cols; c++)
            if (found(r, c, 0)) return true;
    return false;
}

Each printed true, true, false for ABCCED, SEE and ABCB on the grid ["ABCE", "SFCS", "ADEE"]. Writing # over the letter is the visited set, kept in the grid itself, and restoring the letter is the unmark.

Rat in a Maze walks the same way, but wants every path from the top-left corner to the bottom-right one, as strings of D, L, R and U. Trying the four steps in that letter order makes the paths come out already sorted:

def find_paths(mat):
    n = len(mat)
    out, path = [], []
    steps = (("D", 1, 0), ("L", 0, -1), ("R", 0, 1), ("U", -1, 0))   # in letter order: the paths come out sorted

    def walk(r, c):
        if r == n - 1 and c == n - 1:
            out.append("".join(path))
            return
        mat[r][c] = 0                            # mark: the path is on this cell
        for letter, dr, dc in steps:
            nr, nc = r + dr, c + dc
            if 0 <= nr < n and 0 <= nc < n and mat[nr][nc] == 1:
                path.append(letter)
                walk(nr, nc)
                path.pop()
        mat[r][c] = 1                            # unmark on the way back

    if mat[0][0] == 1 and mat[n - 1][n - 1] == 1:
        walk(0, 0)
    return out

On the example maze it returned ['DDRDRR', 'DRDDRR'].

Grids are where this blows up. On the problem's largest board, 6 × 6, all one letter but a B in the corner, the search for a 12-letter word ending in a letter the grid doesn't have made 1,156,248 calls (565 ms) to say no. Two checks before the search fix that kind of input:

from collections import Counter


def exist_pruned(board, word):
    have = Counter(ch for row in board for ch in row)
    if any(have[ch] < k for ch, k in Counter(word).items()):
        return False                             # the board doesn't hold enough of some letter
    if have[word[0]] > have[word[-1]]:
        word = word[::-1]                        # start from the rarer end: fewer cells to start from
    return exist(board, word)

Counting the letters first answers that word without a single call. And starting from the rarer end helps when the word is there: the word ending in the B took 26,195 calls from the A's; read backwards from the one B, it took 56.

Place under rules: N-Queens

N-Queens places n queens on an n × n board so that none attacks another: no two in a row, a column or a diagonal. One queen per row takes care of rows, so each call fills one row and tries its columns. The rest is knowing, in one step, whether a square is attacked. Keep three sets: the columns taken, and the two kinds of diagonal. On a down-right diagonal, row − column is the same for every square; on an up-right one, row + column is:

Two 4 × 4 chequered boards side by side, every square holding a number, with the words queen (1, 2) between them. The left board, headed row − column, runs from −3 at the top right to 3 at the bottom left, each down-right diagonal holding one number; the queen's square at row 1, column 2 is a purple disc reading −1, and the other −1 squares on her diagonal are tinted purple; a pill under it reads diag holds −1. The right board, headed row + column, runs from 0 at the top left to 6 at the bottom right, each up-right diagonal holding one number; the queen's disc reads 3 and the other squares holding 3 are tinted; a pill reads anti holds 3. Focal line: one number per diagonal: taken or not in one step. Title: each diagonal has its own number.
void place(int n, int row, vector<string>& board, vector<bool>& cols, vector<bool>& diag,
           vector<bool>& anti, vector<vector<string>>& out) {
    if (row == n) {                                                // a queen in every row
        out.push_back(board);
        return;
    }
    for (int c = 0; c < n; c++) {
        if (cols[c] || diag[row - c + n - 1] || anti[row + c]) continue;   // attacked: skip it now
        cols[c] = diag[row - c + n - 1] = anti[row + c] = true;
        board[row][c] = 'Q';
        place(n, row + 1, board, cols, diag, anti, out);
        board[row][c] = '.';
        cols[c] = diag[row - c + n - 1] = anti[row + c] = false;
    }
}

vector<vector<string>> solveNQueens(int n) {
    vector<vector<string>> out;
    vector<string> board(n, string(n, '.'));
    vector<bool> cols(n), diag(2 * n - 1), anti(2 * n - 1);       // taken: columns, row - col, row + col
    place(n, 0, board, cols, diag, anti, out);
    return out;
}
def solve_n_queens(n):
    out, board = [], [["."] * n for _ in range(n)]
    cols, diag, anti = set(), set(), set()      # taken: columns, row - col, row + col

    def place(row):
        if row == n:                            # a queen in every row
            out.append(["".join(r) for r in board])
            return
        for c in range(n):
            if c in cols or row - c in diag or row + c in anti:
                continue                        # attacked: skip it now
            cols.add(c)
            diag.add(row - c)
            anti.add(row + c)
            board[row][c] = "Q"
            place(row + 1)
            board[row][c] = "."
            cols.remove(c)
            diag.remove(row - c)
            anti.remove(row + c)

    place(0)
    return out
static void place(int n, int row, char[][] board, boolean[] cols, boolean[] diag, boolean[] anti,
                  List<List<String>> out) {
    if (row == n) {                                                // a queen in every row
        List<String> rows = new ArrayList<>();
        for (char[] r : board) rows.add(new String(r));
        out.add(rows);
        return;
    }
    for (int c = 0; c < n; c++) {
        if (cols[c] || diag[row - c + n - 1] || anti[row + c]) continue;   // attacked: skip it now
        cols[c] = diag[row - c + n - 1] = anti[row + c] = true;
        board[row][c] = 'Q';
        place(n, row + 1, board, cols, diag, anti, out);
        board[row][c] = '.';
        cols[c] = diag[row - c + n - 1] = anti[row + c] = false;
    }
}

static List<List<String>> solveNQueens(int n) {
    char[][] board = new char[n][n];
    for (char[] r : board) Arrays.fill(r, '.');
    List<List<String>> out = new ArrayList<>();
    // taken: columns, row - col, row + col
    place(n, 0, board, new boolean[n], new boolean[2 * n - 1], new boolean[2 * n - 1], out);
    return out;
}
function solveNQueens(n) {
    const out = [], board = Array.from({length: n}, () => new Array(n).fill("."));
    const cols = new Set(), diag = new Set(), anti = new Set();   // taken: columns, row - col, row + col
    function place(row) {
        if (row === n) {                                           // a queen in every row
            out.push(board.map(r => r.join("")));
            return;
        }
        for (let c = 0; c < n; c++) {
            if (cols.has(c) || diag.has(row - c) || anti.has(row + c)) continue;   // attacked: skip it now
            cols.add(c); diag.add(row - c); anti.add(row + c);
            board[row][c] = "Q";
            place(row + 1);
            board[row][c] = ".";
            cols.delete(c); diag.delete(row - c); anti.delete(row + c);
        }
    }
    place(0);
    return out;
}

Each printed the two boards for n = 4 and 92 for n = 8. In C++ and Java the sets are arrays of booleans: row − column runs from −(n − 1) to n − 1, so it's shifted by n − 1 to make an index. Step through the search for 4 to its first board:

Four queens, row by row — Purple: the queens placed. Indigo: the queen just placed. Dark: the squares of the row being filled that a placed queen attacks. Green: every row filled.

How much checking each queen as it's placed saves, counted:

nBoardsCalls, checked as placedCalls, checked at the end
4217341
510543,906
6415355,987
740552960,800
8922,05719,173,961

At 8, checking only full boards took 153 ms, even in C++. A fairer baseline puts one queen in each row and each column, the permutations of the last lesson, and checks only the diagonals at the end: 109,601 calls at 8, still 53 times as many. The search stays exponential, but every branch that breaks a rule is cut where it breaks.

Below, you run it yourself. Click the square for the next queen, record a full board, or back up. The loop tries each row's columns left to right, and a square a queen attacks is refused, naming that queen.

Place the Queens: you run N-Queens

Sudoku: three sets per cell, and stop when it's solved

Sudoku Solver fills a 9 × 9 board so every row, column and 3 × 3 box holds each digit once. The rules give three sets for each empty cell: the digits already in its row, its column and its box. With whole-number division, box r // 3 * 3 + c // 3 (r / 3 * 3 + c / 3 in C++ and Java) numbers the boxes 0 to 8, left to right and top to bottom. Each call fills the next empty cell with a digit in none of the three, and the solver stops the moment the last cell is filled, because the board itself is the answer:

bool fill(vector<vector<char>>& board, vector<array<bool, 10>>& rows, vector<array<bool, 10>>& cols,
          vector<array<bool, 10>>& boxes, int cell) {
    if (cell == 81) return true;                                   // every cell is filled
    int r = cell / 9, c = cell % 9, b = r / 3 * 3 + c / 3;
    if (board[r][c] != '.') return fill(board, rows, cols, boxes, cell + 1);
    for (int d = 1; d <= 9; d++) {
        if (rows[r][d] || cols[c][d] || boxes[b][d]) continue;     // d is already in its row, column or box
        rows[r][d] = cols[c][d] = boxes[b][d] = true;
        board[r][c] = char('0' + d);
        if (fill(board, rows, cols, boxes, cell + 1)) return true; // solved: stop, and keep the board
        board[r][c] = '.';
        rows[r][d] = cols[c][d] = boxes[b][d] = false;
    }
    return false;                                                  // nothing fits: the caller tries its next digit
}

void solveSudoku(vector<vector<char>>& board) {
    vector<array<bool, 10>> rows(9), cols(9), boxes(9);
    for (int r = 0; r < 9; r++)
        for (int c = 0; c < 9; c++)
            if (board[r][c] != '.') {
                int d = board[r][c] - '0';
                rows[r][d] = cols[c][d] = boxes[r / 3 * 3 + c / 3][d] = true;
            }
    fill(board, rows, cols, boxes, 0);
}
def solve_sudoku(board):
    rows = [set() for _ in range(9)]
    cols = [set() for _ in range(9)]
    boxes = [set() for _ in range(9)]
    for r in range(9):
        for c in range(9):
            if board[r][c] != ".":
                rows[r].add(board[r][c])
                cols[c].add(board[r][c])
                boxes[r // 3 * 3 + c // 3].add(board[r][c])

    def fill(cell):
        if cell == 81:
            return True                          # every cell is filled
        r, c = divmod(cell, 9)
        b = r // 3 * 3 + c // 3
        if board[r][c] != ".":
            return fill(cell + 1)
        for d in "123456789":
            if d in rows[r] or d in cols[c] or d in boxes[b]:
                continue                         # d is already in its row, column or box
            rows[r].add(d)
            cols[c].add(d)
            boxes[b].add(d)
            board[r][c] = d
            if fill(cell + 1):
                return True                      # solved: stop, and keep the board
            board[r][c] = "."
            rows[r].remove(d)
            cols[c].remove(d)
            boxes[b].remove(d)
        return False                             # nothing fits: the caller tries its next digit

    fill(0)
static boolean fill(char[][] board, boolean[][] rows, boolean[][] cols, boolean[][] boxes, int cell) {
    if (cell == 81) return true;                                   // every cell is filled
    int r = cell / 9, c = cell % 9, b = r / 3 * 3 + c / 3;
    if (board[r][c] != '.') return fill(board, rows, cols, boxes, cell + 1);
    for (int d = 1; d <= 9; d++) {
        if (rows[r][d] || cols[c][d] || boxes[b][d]) continue;     // d is already in its row, column or box
        rows[r][d] = cols[c][d] = boxes[b][d] = true;
        board[r][c] = (char) ('0' + d);
        if (fill(board, rows, cols, boxes, cell + 1)) return true; // solved: stop, and keep the board
        board[r][c] = '.';
        rows[r][d] = cols[c][d] = boxes[b][d] = false;
    }
    return false;                                                  // nothing fits: the caller tries its next digit
}

static void solveSudoku(char[][] board) {
    boolean[][] rows = new boolean[9][10], cols = new boolean[9][10], boxes = new boolean[9][10];
    for (int r = 0; r < 9; r++)
        for (int c = 0; c < 9; c++)
            if (board[r][c] != '.') {
                int d = board[r][c] - '0';
                rows[r][d] = cols[c][d] = boxes[r / 3 * 3 + c / 3][d] = true;
            }
    fill(board, rows, cols, boxes, 0);
}
function solveSudoku(board) {
    const rows = Array.from({length: 9}, () => new Set());
    const cols = Array.from({length: 9}, () => new Set());
    const boxes = Array.from({length: 9}, () => new Set());
    const boxOf = (r, c) => Math.floor(r / 3) * 3 + Math.floor(c / 3);
    for (let r = 0; r < 9; r++)
        for (let c = 0; c < 9; c++)
            if (board[r][c] !== ".") {
                rows[r].add(board[r][c]); cols[c].add(board[r][c]); boxes[boxOf(r, c)].add(board[r][c]);
            }
    function fill(cell) {
        if (cell === 81) return true;                              // every cell is filled
        const r = Math.floor(cell / 9), c = cell % 9, b = boxOf(r, c);
        if (board[r][c] !== ".") return fill(cell + 1);
        for (const d of "123456789") {
            if (rows[r].has(d) || cols[c].has(d) || boxes[b].has(d)) continue;   // already in its row, column or box
            rows[r].add(d); cols[c].add(d); boxes[b].add(d);
            board[r][c] = d;
            if (fill(cell + 1)) return true;                       // solved: stop, and keep the board
            board[r][c] = ".";
            rows[r].delete(d); cols[c].delete(d); boxes[b].delete(d);
        }
        return false;                                              // nothing fits: the caller tries its next digit
    }
    fill(0);
}

Each filled LeetCode's example, first row 53..7.... becoming 534678912.

Which cell to fill next matters a lot. Reading order takes the next empty cell, whatever it allows. Taking the empty cell with the fewest digits that fit instead means fewer guesses, and a wrong guess shows up sooner:

def solve_fewest_first(board):
    rows = [set() for _ in range(9)]
    cols = [set() for _ in range(9)]
    boxes = [set() for _ in range(9)]
    empty = set()
    for r in range(9):
        for c in range(9):
            if board[r][c] == ".":
                empty.add((r, c))
            else:
                rows[r].add(board[r][c])
                cols[c].add(board[r][c])
                boxes[r // 3 * 3 + c // 3].add(board[r][c])

    def fits(r, c):
        b = r // 3 * 3 + c // 3
        return [d for d in "123456789" if d not in rows[r] and d not in cols[c] and d not in boxes[b]]

    def fill():
        if not empty:
            return True
        r, c = min(empty, key=lambda cell: len(fits(*cell)))   # the most constrained empty cell
        b = r // 3 * 3 + c // 3
        empty.remove((r, c))
        for d in fits(r, c):
            rows[r].add(d)
            cols[c].add(d)
            boxes[b].add(d)
            board[r][c] = d
            if fill():
                return True
            board[r][c] = "."
            rows[r].remove(d)
            cols[c].remove(d)
            boxes[b].remove(d)
        empty.add((r, c))
        return False

    fill()
PuzzleCalls, reading orderCalls, fewest first
LeetCode's example6,42852
Arto Inkala's72,0974,326

On LeetCode's example it went from 9 to 3 ms. On Inkala's the calls fell 17-fold, but the time rose from 103 ms to 294: every call scans all the empty cells to find the fewest, and in Python the scan cost more than the calls it saved. Fewer calls is the goal; how cheaply you pick the next cell decides whether time follows.

M-Coloring: a colour per vertex

M-Coloring Problem asks if a graph's vertices can take one of m colours each, with no edge joining two of the same colour. Each call colours the next vertex with a colour none of its neighbours has:

def graph_coloring(v, edges, m):
    nbrs = [[] for _ in range(v)]
    for a, b in edges:
        nbrs[a].append(b)
        nbrs[b].append(a)
    color = [0] * v                              # 0: no colour yet

    def paint(u):
        if u == v:
            return True                          # every vertex has a colour
        for k in range(1, m + 1):
            if all(color[w] != k for w in nbrs[u]):
                color[u] = k                     # no neighbour has colour k
                if paint(u + 1):
                    return True
                color[u] = 0
        return False

    return paint(0)

For the example's 4 vertices and edges [[0, 1], [1, 3], [2, 3], [3, 0], [0, 2]], 3 colours work and 2 don't: it returned True and False. Vertices 0, 1 and 3 all touch each other, so they need three colours between them.

Spotting it, and when it breaks

The clues. "Find a path on the board that spells", "place n pieces so that none", "fill the grid so every row", "colour so that no two neighbours": choices on a board, each bound by rules. The inputs are tiny: a 6 × 6 grid, n up to 9, one 9 × 9 board, 10 vertices.

The near-misses. The shortest path on a grid: breadth-first search, not backtracking (Shortest Paths with BFS). Counting paths that only go right or down: a table (Grid DP). Many words on the same board: a trie walked alongside the search (Tries for Searching Many Words).

When it breaks. These stay exponential. Every check you add cuts branches, but the worst input still grows the tree fast, which is why the limits are this small. Count first, start from the most constrained choice, and cut a branch the moment it breaks a rule.

Bugs that cost a submission

Not putting the letter back.

keep, board[r][c] = board[r][c], "#"
ok = (found(r + 1, c, k + 1) or found(r - 1, c, k + 1)
      or found(r, c + 1, k + 1) or found(r, c - 1, k + 1))
return ok                                # the cell stays "#"

On the grid ["AAA", "AAB"], exist for "AAB" returned False; with the letter put back it returns True. The first starts fail and leave # behind, and the path that works needs those cells.

One diagonal set.

if c in cols or row - c in diag:   # row + c never checked
    continue

For n = 4 it returned 7 boards instead of 2: queens on the same up-right diagonal slip through.

Not stopping at the solution.

board[r][c] = d
fill(cell + 1)                       # its answer ignored
board[r][c] = "."

The board came back with its first row still 53..7....: the search filled every cell, then kept backing out of the solution, emptying each cell on the way.

A maze path that doesn't mark its cells.

for letter, dr, dc in steps:        # mat[r][c] = 0 missing
    nr, nc = r + dr, c + dc

On the example maze, Python raised RecursionError: with nothing marked, the rat stepped back and forth between two open cells until the stack ran out.

Check yourself

Quiz

1 / 6

Multiple choice

In Word Search, why does the code put the letter back after exploring from a cell?

Practice problems

In this order:

  1. Word Search: mark the cell, try the neighbours, unmark.
  2. Rat in a Maze: every path, the steps tried in letter order.
  3. N-Queens: a row per call, three sets for the attacks.
  4. M-Coloring Problem: a colour per vertex, checked against its neighbours.
  5. Sudoku Solver: three sets per cell, and stop at the solution.

Then the section quiz, across all four lessons.