Files
termdoku/internal/solver/solver.go

289 lines
5.6 KiB
Go

package solver
import "time"
// Grid matches generator's grid representation
type Grid [9][9]uint8
// Solve attempts to fill the grid in-place using backtracking.
// Returns whether a solution was found before timeout.
func Solve(g *Grid, timeout time.Duration) bool {
deadline := time.Now().Add(timeout)
return solveBacktrack(g, deadline)
}
// CountSolutions counts up to maxCount solutions for uniqueness check.
func CountSolutions(g Grid, timeout time.Duration, maxCount int) int {
deadline := time.Now().Add(timeout)
count := 0
var dfs func(*Grid) bool
dfs = func(cur *Grid) bool {
if time.Now().After(deadline) {
return true
}
// Use most-constrained heuristic for faster counting
row, col, ok := findMostConstrained(*cur)
if !ok {
count++
return count >= maxCount
}
cands := candidates(*cur, row, col)
if len(cands) == 0 {
return false
}
for _, v := range cands {
cur[row][col] = v
if dfs(cur) {
return true
}
cur[row][col] = 0
}
return false
}
copyGrid := g
dfs(&copyGrid)
return count
}
func solveBacktrack(g *Grid, deadline time.Time) bool {
if time.Now().After(deadline) {
return false
}
// Use most-constrained-first heuristic for better performance
row, col, ok := findMostConstrained(*g)
if !ok {
return true
}
cands := candidates(*g, row, col)
// If no candidates available, this path is invalid
if len(cands) == 0 {
return false
}
for _, v := range cands {
g[row][col] = v
if solveBacktrack(g, deadline) {
return true
}
g[row][col] = 0
}
return false
}
func findEmpty(g Grid) (int, int, bool) {
for r := 0; r < 9; r++ {
for c := 0; c < 9; c++ {
if g[r][c] == 0 {
return r, c, true
}
}
}
return 0, 0, false
}
// findMostConstrained finds the empty cell with the fewest candidates.
// This heuristic significantly improves solving performance.
func findMostConstrained(g Grid) (int, int, bool) {
minCands := 10
bestRow, bestCol := -1, -1
found := false
for r := 0; r < 9; r++ {
for c := 0; c < 9; c++ {
if g[r][c] == 0 {
cands := candidates(g, r, c)
if len(cands) < minCands {
minCands = len(cands)
bestRow = r
bestCol = c
found = true
// If we find a cell with only one candidate, use it immediately
if minCands == 1 {
return bestRow, bestCol, true
}
}
}
}
}
return bestRow, bestCol, found
}
func candidates(g Grid, row, col int) []uint8 {
used := [10]bool{}
for i := 0; i < 9; i++ {
used[g[row][i]] = true
used[g[i][col]] = true
}
r0 := (row / 3) * 3
c0 := (col / 3) * 3
for r := r0; r < r0+3; r++ {
for c := c0; c < c0+3; c++ {
used[g[r][c]] = true
}
}
var out []uint8
for v := 1; v <= 9; v++ {
if !used[v] {
out = append(out, uint8(v))
}
}
return out
}
// IsValid checks if the grid is in a valid state (no conflicts).
func IsValid(g Grid) bool {
// Check rows
for r := 0; r < 9; r++ {
seen := [10]bool{}
for c := 0; c < 9; c++ {
v := g[r][c]
if v == 0 {
continue
}
if seen[v] {
return false
}
seen[v] = true
}
}
// Check columns
for c := 0; c < 9; c++ {
seen := [10]bool{}
for r := 0; r < 9; r++ {
v := g[r][c]
if v == 0 {
continue
}
if seen[v] {
return false
}
seen[v] = true
}
}
// Check 3x3 blocks
for br := 0; br < 3; br++ {
for bc := 0; bc < 3; bc++ {
seen := [10]bool{}
for r := br * 3; r < br*3+3; r++ {
for c := bc * 3; c < bc*3+3; c++ {
v := g[r][c]
if v == 0 {
continue
}
if seen[v] {
return false
}
seen[v] = true
}
}
}
}
return true
}
// IsSolved checks if the grid is completely filled and valid.
func IsSolved(g Grid) bool {
if !IsValid(g) {
return false
}
for r := 0; r < 9; r++ {
for c := 0; c < 9; c++ {
if g[r][c] == 0 {
return false
}
}
}
return true
}
// SolveStep represents a single step in the solving process.
type SolveStep struct {
Row int
Col int
Value uint8
Candidates []uint8
StepNumber int
}
// SolveWithSteps solves the puzzle and returns all steps taken.
func SolveWithSteps(g Grid, timeout time.Duration) ([]SolveStep, bool) {
deadline := time.Now().Add(timeout)
var steps []SolveStep
stepNum := 0
var solve func(*Grid) bool
solve = func(cur *Grid) bool {
if time.Now().After(deadline) {
return false
}
row, col, ok := findMostConstrained(*cur)
if !ok {
return true
}
cands := candidates(*cur, row, col)
if len(cands) == 0 {
return false
}
for _, v := range cands {
stepNum++
steps = append(steps, SolveStep{
Row: row,
Col: col,
Value: v,
Candidates: cands,
StepNumber: stepNum,
})
cur[row][col] = v
if solve(cur) {
return true
}
cur[row][col] = 0
}
return false
}
copyGrid := g
success := solve(&copyGrid)
return steps, success
}
// GetAllSolutions returns all possible solutions for a puzzle (up to maxSolutions).
func GetAllSolutions(g Grid, timeout time.Duration, maxSolutions int) []Grid {
deadline := time.Now().Add(timeout)
var solutions []Grid
var dfs func(*Grid)
dfs = func(cur *Grid) {
if time.Now().After(deadline) || len(solutions) >= maxSolutions {
return
}
row, col, ok := findMostConstrained(*cur)
if !ok {
// Found a solution, save a copy
var solution Grid
for r := 0; r < 9; r++ {
for c := 0; c < 9; c++ {
solution[r][c] = cur[r][c]
}
}
solutions = append(solutions, solution)
return
}
cands := candidates(*cur, row, col)
for _, v := range cands {
cur[row][col] = v
dfs(cur)
cur[row][col] = 0
}
}
copyGrid := g
dfs(&copyGrid)
return solutions
}