289 lines
5.6 KiB
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(©Grid)
|
|
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(©Grid)
|
|
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(©Grid)
|
|
return solutions
|
|
}
|