(Feat): Initial Commit, Termdoku
This commit is contained in:
288
internal/solver/solver.go
Normal file
288
internal/solver/solver.go
Normal file
@@ -0,0 +1,288 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user