Brute Force

Enumerate every candidate and check it. The first solution to write for any problem: it is the reference for stress testing, it reveals patterns on small inputs, and it is the full solution when the constraints allow.

What can be enumerated in ~1 second

ObjectCountFeasible
subsets (with work), (with )
permutations ( with work)
pairs / triples / /
subsets of size check the number,
strings over
divisors of for always
partitions of ,

Enumeration templates

// all subsets
for (int mask = 0; mask < (1 << n); mask++) { /* bit i set => take a[i] */ }
 
// all permutations (start from sorted to get all)
sort(p.begin(), p.end());
do { /* use p */ } while (next_permutation(p.begin(), p.end()));
 
// all k-subsets (Gosper)
for (int s = (1 << k) - 1; s < (1 << n); ) {
    /* use s */
    int c = s & -s, r = s + c; s = (((r ^ s) >> 2) / c) | r;
}
 
// recursion over choices with pruning -> backtracking

Smarter brute force

  • Enumerate the answer instead of the input: “is the answer ?” then try each.
  • Enumerate one variable, compute the other: fixes .
  • Meet in the middle: split into halves of .
  • Symmetry breaking: fix the first element, sort to avoid duplicates.
  • Pruning: stop when the partial cost already exceeds the best (branch and bound).

Brute force as a tool

  1. Print the answers for and search the sequence (OEIS-style pattern finding).
  2. Stress-test the main solution against it with random small inputs.
  3. Verify a greedy rule on all small cases before proving it.

Pitfalls

  • Count the operations: is fine, is borderline.
  • next_permutation skips permutations if you start from an unsorted array.

Backtracking

Build a solution incrementally; abandon a partial solution (“backtrack”) as soon as it cannot be completed.

The skeleton

void solve(State& s, int depth) {
    if (complete(s)) { record(s); return; }
    if (!feasible(s)) return;                          // PRUNE
    for (Choice c : candidates(s)) {
        apply(s, c);
        solve(s, depth + 1);
        undo(s, c);                                    // the "backtrack"
    }
}

The undo is what makes it backtracking rather than brute force: state is mutated in place and restored, so no copying is needed.

Pruning — where all the performance is

Without pruning, backtracking is just exponential enumeration. The techniques, in rough order of value:

TechniqueIdea
Constraint propagationafter each choice, eliminate now-impossible options
Most-constrained variable first (MRV)branch on the variable with the fewest remaining options
Least-constraining valuetry the value that eliminates the fewest options for others
Boundingif the best possible completion is worse than the incumbent, prune
Symmetry breakingfix a canonical order among interchangeable choices
Forward checkingdetect a variable with zero remaining options immediately
Memoizationcache equivalent states
Early failure detectioncheck global feasibility (counts, parity, connectivity)

MRV is the single highest-value heuristic. Branching on the most constrained variable makes the tree narrow at the top, where it matters most. It is what turns a Sudoku solver from seconds to microseconds.

Symmetry breaking

If items are interchangeable, fix a canonical order so each solution is generated once:

// assigning items to groups: item i may only open group g if all groups < g are used
for (int g = 0; g <= usedGroups && g < maxGroups; g++) { ... }

Without this, a partition into groups is produced times. This is often a 10-100× speedup and is the first thing to add.

Other common symmetries: reflections and rotations of a board, permutations of identical pieces, and the choice of which element goes “first”.

Classic backtracking problems

ProblemKey pruning
N-Queenscolumn/diagonal bitmasks; place row by row
SudokuMRV + constraint propagation, or DLX
Graph colouringorder vertices by degree; symmetry-break the colours
Knight’s tourWarnsdorff’s rule (fewest onward moves first)
Subset sum / partitionsort descending; skip duplicates; bound the remaining sum
Hamiltonian pathprune on degree-0 or two degree-1 vertices remaining
Exact cover (pentominoes)Dancing Links
Crossword / word placementMRV on the most constrained slot
Maximum cliqueBron-Kerbosch with a colouring bound

N-Queens with bitmasks

void queens(int row, int cols, int d1, int d2, int n, int& count) {
    if (row == n) { count++; return; }
    int avail = ~(cols | d1 | d2) & ((1 << n) - 1);
    while (avail) {
        int bit = avail & -avail;
        avail ^= bit;
        queens(row + 1, cols | bit, (d1 | bit) << 1, (d2 | bit) >> 1, n, count);
    }
}

Three integers encode all the constraints, and the shifts propagate the diagonals automatically. in well under a second. (Proof that the shifts correctly track the diagonals: N-Queens, below, where this is the main algorithm.)

Warnsdorff’s rule

For a knight’s tour, always move to the square with the fewest onward moves. This heuristic finds a tour on boards up to almost without backtracking — a striking demonstration that a good ordering can dominate everything else.

Skipping duplicates

When the candidate list contains equal values, generate each distinct choice once:

sort(a.begin(), a.end());
for (int i = 0; i < n; i++) {
    if (i > 0 && a[i] == a[i-1] && !used[i-1]) continue;    // skip duplicates
    ...
}

The !used[i-1] condition forces equal elements to be used in index order. This is the standard idiom for “permutations with duplicates” and “subsets with duplicates”.

Backtracking vs the alternatives

SituationMethod
Small , need all solutionsbacktracking
Small , need one solutionbacktracking with early exit
Optimising, with a good boundbranch and bound
Overlapping subproblemsmemoization / DP
, subset structurebitmask DP
, splittablemeet in the middle
Exact cover structureDancing Links
Boolean constraintsa SAT solver, or 2-SAT if binary clauses
Too big for exactheuristics

Pruning

Idea and use

Skip a search branch only when it cannot contain a feasible or better answer.

Method and cost

Use infeasibility checks, admissible objective bounds, symmetry breaking, and dominance. For maximizing clique size, prune when current_size+candidate_count<=best; a proper coloring of candidates gives a tighter upper bound.

Correctness and pitfalls

Heuristic move ordering is safe, but heuristic rejection may lose the optimum. If an incumbent equals a bound, pruning depends on whether every optimal witness or only one is needed.

Work it out

Design a lower bound for bin packing from remaining total size and test that it never exceeds the true optimum on tiny inputs.

Backtracking Puzzles

Bitmask Sudoku solver, Warnsdorff knight’s tour, counting simple paths in a grid maze, and word search on a letter grid.

bool ok = solveSudoku(g);                 // g: 9 strings of 9 chars, '.' or '0' for empty; filled in place
auto tour = knightsTour(n, sr, sc);       // n x n board, order[r][c] = move number, {} if none found
i64 p = countMazePaths(grid);             // simple paths from (0,0) to (n-1,m-1) through '.', 4 directions
bool w = wordSearch(grid, word);          // adjacent cells, each used at most once

Complexity: exponential in the worst case; Sudoku solves hard instances in milliseconds (most constrained cell first + bitmasks); Warnsdorff is linear per attempt and almost always succeeds for .
Pruning order matters more than anything: pick the variable with the fewest options, keep candidate sets as bitmasks, undo changes on return.

Implementation

bool solveSudoku(vector<string>& g) {
    array<int, 9> row{}, col{}, box{};
    vector<pair<int, int>> empty;
    for (int r = 0; r < 9; r++) {
        for (int c = 0; c < 9; c++) {
            if (g[r][c] == '.' || g[r][c] == '0') {
                g[r][c] = '.';
                empty.push_back({r, c});
                continue;
            }
            int bit = 1 << (g[r][c] - '1');
            if ((row[r] | col[c] | box[r / 3 * 3 + c / 3]) & bit) return false;
            row[r] |= bit, col[c] |= bit, box[r / 3 * 3 + c / 3] |= bit;
        }
    }
    auto dfs = [&](auto&& self, int left) -> bool {
        if (left == 0) return true;
        int best = -1, bestCnt = 10, bestMask = 0;
        for (int i = 0; i < left; i++) {   // most constrained empty cell
            auto [r, c] = empty[i];
            int mask = ~(row[r] | col[c] | box[r / 3 * 3 + c / 3]) & 511, cnt = __builtin_popcount(mask);
            if (cnt < bestCnt) best = i, bestCnt = cnt, bestMask = mask;
        }
        if (bestCnt == 0) return false;
        swap(empty[best], empty[left - 1]);
        auto [r, c] = empty[left - 1];
        for (int m = bestMask; m; m &= m - 1) {
            int bit = m & -m;
            row[r] |= bit, col[c] |= bit, box[r / 3 * 3 + c / 3] |= bit;
            g[r][c] = char('1' + __builtin_ctz(bit));
            if (self(self, left - 1)) return true;
            row[r] ^= bit, col[c] ^= bit, box[r / 3 * 3 + c / 3] ^= bit;
        }
        g[r][c] = '.';
        swap(empty[best], empty[left - 1]);
        return false;
    };
    return dfs(dfs, empty.size());
}
 
vector<vector<int>> knightsTour(int n, int sr, int sc) {
    const int dr[] = {1, 2, 2, 1, -1, -2, -2, -1}, dc[] = {2, 1, -1, -2, -2, -1, 1, 2};
    for (int attempt = 0; attempt < 8; attempt++) {   // rotate the tie-break order on failure
        vector<vector<int>> order(n, vector<int>(n, -1));
        int r = sr, c = sc;
        order[r][c] = 0;
        auto degree = [&](int x, int y) {
            int d = 0;
            for (int k = 0; k < 8; k++) {
                int nx = x + dr[k], ny = y + dc[k];
                d += nx >= 0 && ny >= 0 && nx < n && ny < n && order[nx][ny] == -1;
            }
            return d;
        };
        bool ok = true;
        for (int step = 1; step < n * n && ok; step++) {
            int best = -1, bestDeg = 9;
            for (int t = 0; t < 8; t++) {
                int k = (t + attempt) % 8, nx = r + dr[k], ny = c + dc[k];
                if (nx < 0 || ny < 0 || nx >= n || ny >= n || order[nx][ny] != -1) continue;
                int d = degree(nx, ny);
                if (d < bestDeg) bestDeg = d, best = k;
            }
            if (best == -1) ok = false;
            else r += dr[best], c += dc[best], order[r][c] = step;
        }
        if (ok) return order;
    }
    return {};
}
 
i64 countMazePaths(const vector<string>& grid) {
    int n = grid.size(), m = grid[0].size();
    vector<vector<char>> used(n, vector<char>(m, 0));
    i64 count = 0;
    auto dfs = [&](auto&& self, int r, int c) -> void {
        if (r == n - 1 && c == m - 1) {
            count++;
            return;
        }
        used[r][c] = 1;
        const int dr[] = {1, -1, 0, 0}, dc[] = {0, 0, 1, -1};
        for (int k = 0; k < 4; k++) {
            int nr = r + dr[k], nc = c + dc[k];
            if (nr >= 0 && nc >= 0 && nr < n && nc < m && grid[nr][nc] == '.' && !used[nr][nc]) self(self, nr, nc);
        }
        used[r][c] = 0;
    };
    if (grid[0][0] == '.' && grid[n - 1][m - 1] == '.') dfs(dfs, 0, 0);
    return count;
}
 
bool wordSearch(vector<string> grid, const string& word) {
    int n = grid.size(), m = grid[0].size();
    auto dfs = [&](auto&& self, int r, int c, int i) -> bool {
        if (i == (int)word.size()) return true;
        if (r < 0 || c < 0 || r >= n || c >= m || grid[r][c] != word[i]) return false;
        char saved = grid[r][c];
        grid[r][c] = '#';
        bool found = self(self, r + 1, c, i + 1) || self(self, r - 1, c, i + 1) || self(self, r, c + 1, i + 1) || self(self, r, c - 1, i + 1);
        grid[r][c] = saved;
        return found;
    };
    for (int r = 0; r < n; r++) {
        for (int c = 0; c < m; c++) {
            if (dfs(dfs, r, c, 0)) return true;
        }
    }
    return word.empty();
}

N-Queens

Problem. Place queens on an board so that no two attack each other (no shared row, column or diagonal). Count the solutions, or find one.

Backtracking with bitmasks — the standard solution

Place one queen per row; track occupied columns and both diagonal families as bitmasks — this is exactly queens from Backtracking above:

// void queens(int row, int cols, int d1, int d2, int n, int& count) — see Backtracking
// call: int count = 0; queens(0, 0, 0, 0, n, count);

avail & -avail isolates the lowest available column, so the loop visits each candidate once. runs in well under a second; takes a few seconds.

Solution counts

SolutionsSolutions
11892
209352
3010724
421214 200
510152 279 184
6420
74027 (the largest computed)

No closed form is known, and counting is believed hard. was computed in 2016 with a large distributed effort.

Constructing one solution in

Counting is hard; finding a single solution is easy. Explicit formulas exist for every :

  • even, : place queens at for and for the rest.
  • Other cases have similar formulas with small adjustments.

So “output any valid placement” is and needs no search at all — worth knowing, because contest versions usually ask for a solution, not the count.

Symmetry breaking

The board has 8 symmetries (the dihedral group ). Restricting the first queen to the first half of the first row roughly halves the search, and correcting for the fixed points recovers the exact count.

More aggressive: classify solutions by their symmetry class (asymmetric, 180°-symmetric, 90°-symmetric) and count each with the appropriate multiplier — a Burnside argument.

As exact cover

N-Queens is an exact cover problem with:

  • primary columns (must be covered exactly once): each rank, each file;
  • secondary columns (at most once): each of the diagonals.

Dancing Links with optional columns solves it directly, and this is the standard demonstration of secondary columns.

Variants

VariantNote
Count solutionsno closed form; by search
Find any solution construction
Solutions up to symmetry”fundamental” solutions; 12 for
Queens on a torussolvable iff
superqueens (queen + knight moves)much more constrained
Minimum queens to dominate the boarda different (domination) problem
Maximum non-attacking rookstrivially
Maximum non-attacking knights — one colour class
Maximum non-attacking bishops
Peaceable queens (two armies)an open problem

Why it is worth knowing

It is the canonical demonstration of three ideas:

  1. Bitmask state — three integers encode all the constraints, and the transitions are shifts.
  2. Backtracking with constraint checks — the pruning is free.
  3. Counting is harder than constructing — a recurring asymmetry (see also permanent vs determinant, and matchings).

Knight’s Tour

Problem. Move a knight over every square of an board exactly once. A closed tour returns to the start; an open tour need not.

This is a Hamiltonian path problem on the knight-move graph — NP-hard in general, but easy here because the graph is highly structured.

Warnsdorff’s rule — the key heuristic

Always move to the square with the fewest onward moves.

This is exactly knightsTour from Backtracking Puzzles above (which additionally rotates the tie-break order on failure and retries, a cheap robustness improvement over the bare greedy rule).

It almost never backtracks. Warnsdorff’s rule finds tours on boards up to essentially instantly, despite the underlying problem being NP-hard. No proof of correctness is known — it is an empirically reliable heuristic, not a guaranteed algorithm (see the divide-and-conquer construction below for a method that is guaranteed).

The intuition: squares with few remaining exits become unreachable if not visited soon, so visit them first. It is exactly the most-constrained-first heuristic from backtracking.

For robustness, break ties by distance from the centre, and add backtracking as a fallback.

Existence

BoardOpen tourClosed tour
, yesyes iff is even
, oddyesno
no (the centre is isolated)no
nono
yesno
, generalSchwenk’s theorem gives the exact conditions

Why no closed tour on odd boards: a knight alternates colours every move. A closed tour has moves, so it needs equally many squares of each colour — impossible when is odd. A clean parity argument.

Schwenk’s theorem. An board () has a closed tour unless: and are both odd; or ; or and .

Divide and conquer

For large boards, split into quadrants, find a tour in each (recursively or from a base case), and stitch them together by breaking one edge in each and reconnecting. Gives an construction with a guarantee, rather than relying on a heuristic.

Counting tours

The number of closed tours on is 26 534 728 821 064 — computed by exhaustive search with heavy symmetry reduction. There is no formula, and counting is far harder than finding.

Variants

VariantNote
Closed tourneeds even
Tour from a specific squareusually possible on large boards
Tour visiting squares in a given ordermuch harder
Knight’s distance between two squaresa closed form exists, or BFS
Minimum knight moves on an infinite boarda formula based on
Knight moves on a torus / 3D boarddifferent existence conditions
Other pieces (camel, giraffe)same framework, different move sets

Knight distance on an infinite board is worth knowing: for with , the answer is a small case analysis around adjusted for parity, with special cases near the origin. Contest problems often ask exactly this — and a BFS over a bounded region also works.

Why it is worth knowing

It is the standard illustration that a good ordering heuristic can beat exponential search. Warnsdorff’s rule has no proof of correctness, yet it works on every board anyone has tested — a useful reminder that “NP-hard” describes the worst case, not the instances you meet.

Pruning and branch and bound

Exponential searches become practical when most branches are cut early. Every pruning rule must be safe: it may only discard branches that cannot beat (or reach) the best answer.

RuleSafe because
bound: optimistic estimate best so farthe branch cannot improve the answer
feasibility: remaining resources cannot satisfy the constraintsno completion exists
symmetry breaking: force an order on interchangeable choicesevery solution has a representative in the allowed order
dominance: state A is at least as good as B in every respectany completion of B also completes A at least as well
memoization of failed statesthe same subproblem fails again
good ordering: most constrained first, best value firstfinds strong solutions early, so the bound prunes more

Branch and bound for knapsack

Sort by value density; the fractional relaxation of the remaining items is an upper bound.

long long knapsackBB(vector<pair<int, int>> items, int cap) {   // {weight, value}; branch and bound with fractional bound
    sort(all(items), [](auto a, auto b) { return (long long)a.second * b.first > (long long)b.second * a.first; });
    int n = sz(items); long long best = 0;
    auto bound = [&](int i, int w, long long v) {            // fractional relaxation of the rest
        double b = v; int room = cap - w;
        for (; i < n && items[i].first <= room; i++) room -= items[i].first, b += items[i].second;
        if (i < n) b += (double)items[i].second * room / items[i].first;
        return b;
    };
    auto rec = [&](auto self, int i, int w, long long v) -> void {
        best = max(best, v);
        if (i == n || bound(i, w, v) <= best) return;
        if (w + items[i].first <= cap) self(self, i + 1, w + items[i].first, v + items[i].second);
        self(self, i + 1, w, v);
    };
    rec(rec, 0, 0, 0);
    return best;
}

Related: Backtracking, Informed Search, Maximum Clique.