Backtracking on the first blank is correct but can crawl on hard puzzles. Today you switch to picking the cell with the fewest candidates - the MRV heuristic - which turns a solver that stalls on the hardest puzzles into one that finishes them in a blink.
Choose the empty cell with the fewest candidates and branch there instead of the first blank.
The backtracking search wastes effort when it branches on a cell with many options: each guess spawns many subtrees, most of them doomed. The classic fix is the minimum-remaining-values heuristic, or MRV: always branch on the empty cell with the fewest candidates. A cell with one candidate is forced (no guessing at all); a cell with two splits the search only two ways. Choosing the tightest cell keeps the branching factor as small as the puzzle allows.
The change to the solver is one line - branch on MostConstrained instead of
FirstEmpty, breaking ties by lowest index to stay deterministic - but its effect is
dramatic. The plain solver could grind for a very long time on a puzzle like the
21-clue “hardest” one; with MRV the same search settles it almost instantly, because
it always attacks the puzzle where it is most pinned down. Same algorithm, same
exact answers, a night-and-day difference in speed.
// among blanks, the one with the smallest candidate set (ties: lowest index)func MostConstrained(g [81]int) int {best, bestN := -1, 10for i := 0; i < 81; i++ {if g[i] != 0 { continue }if n := Candidates(g, i).Size(); n < bestN { best, bestN = i, n }}return best}// the solver is unchanged except it branches on MostConstrained, not FirstEmpty