Sudoku: Difference between revisions
Content added Content deleted
Line 4,139: | Line 4,139: | ||
<!-- By Martin Freedman, 27/11/2021 --> |
<!-- By Martin Freedman, 27/11/2021 --> |
||
<lang fsharp>// https://norvig.com/sudoku.html |
<lang fsharp>// https://norvig.com/sudoku.html |
||
// using array O(1) lookup & mutable instead of map O(logn) immutable - now 6 times faster |
|||
module |
module SudokuCPSArray |
||
open System |
open System |
||
/// from 11 to 99 as squares key in |
/// from 11 to 99 as squares key maps to 0 to 80 in arrays |
||
let key a b = |
let key a b = (9*a + b) - 10 |
||
/// Keys generator |
/// Keys generator |
||
let cross ax bx = [| for a in ax do for b in bx do key a b |] |
let cross ax bx = [| for a in ax do for b in bx do key a b |] |
||
// constants |
|||
let digits = [|1..9|] |
let digits = [|1..9|] |
||
let rows = digits |
let rows = digits |
||
Line 4,157: | Line 4,157: | ||
let squares = cross rows cols |
let squares = cross rows cols |
||
// List of all row, cols and boxes: aka units |
/// List of all row, cols and boxes: aka units |
||
let unitlist = |
let unitlist = |
||
[for c in cols -> cross rows [|c|] ]@ |
[for c in cols -> cross rows [|c|] ]@ |
||
Line 4,165: | Line 4,165: | ||
/// Dictionary of units for each square |
/// Dictionary of units for each square |
||
let units = |
let units = |
||
[for s in squares do |
[|for s in squares do [| for u in unitlist do if u |> Array.contains s then u |] |] |
||
/// Dictionary of all peer squares in the relevant units wrt square in question |
/// Dictionary of all peer squares in the relevant units wrt square in question |
||
let peers = |
let peers = |
||
⚫ | |||
let tuple2 a b = a,b |
|||
⚫ | |||
⚫ | |||
/// folds folder returning Some on completion or returns None if not |
/// folds folder returning Some on completion or returns None if not |
||
Line 4,183: | Line 4,180: | ||
/// Assign digit d to values[s] and propagate (via eliminate) |
/// Assign digit d to values[s] and propagate (via eliminate) |
||
/// Return Some values, except None if a contradiction is detected. |
/// Return Some values, except None if a contradiction is detected. |
||
let rec assign (values: |
let rec assign (values:int[][]) (s) d = |
||
values[s] |
values[s] |
||
|> |
|> Array.filter ((<>)d) |
||
|> List. |
|> List.ofArray |> all (fun vx d1 -> eliminate vx s d1) (Some values) |
||
/// Eliminate digit d from values[s] and propagate when values[s] size is 1. |
/// Eliminate digit d from values[s] and propagate when values[s] size is 1. |
||
/// Return Some values, except return None if a contradiction is detected. |
/// Return Some values, except return None if a contradiction is detected. |
||
and eliminate (values: |
and eliminate (values:int[][]) s d = |
||
let peerElim ( |
let peerElim (values1:int[][]) = // If a square s is reduced to one value d, then *eliminate* d from the peers. |
||
match Seq.length |
match Seq.length values1[s] with |
||
| 0 -> None // removed last value |
| 0 -> None // contradiction - removed last value |
||
| 1 -> peers[s] |> List.ofArray |> all (fun |
| 1 -> peers[s] |> List.ofArray |> all (fun vx1 s1 -> eliminate vx1 s1 (values1[s] |> Seq.head) ) (Some values1) |
||
| _ -> Some |
| _ -> Some values1 |
||
let unitsElim |
let unitsElim values1 = // If a unit u is reduced to only one place for a value d, then *assign* it there. |
||
units[s] |
units[s] |
||
|> List.ofArray |
|> List.ofArray |
||
|> all (fun (vx1:int[][]) u -> |
|||
let sx = [for s in u do if vx1[s] |> Seq.contains d then s] |
|||
match Seq.length sx with |
|||
| 0 -> None |
| 0 -> None |
||
| 1 -> assign |
| 1 -> assign vx1 (Seq.head sx) d |
||
| _ -> Some |
| _ -> Some vx1) (Some values1) |
||
match values[s] |> Seq.contains d with |
match values[s] |> Seq.contains d with |
||
| false -> Some values // Already eliminated, nothing to do |
| false -> Some values // Already eliminated, nothing to do |
||
| true -> |
| true -> |
||
values[s] <- values[s]|> Array.filter ((<>)d) |
|||
values |
|||
⚫ | |||
|> peerElim |
|||
⚫ | |||
/// Convert grid into a Map of {square: char} with "0","."or"," for empties. |
/// Convert grid into a Map of {square: char} with "0","."or"," for empties. |
||
Line 4,218: | Line 4,217: | ||
if Seq.length cells = 81 then cells |> Seq.zip squares |> Map.ofSeq |> Some else None |
if Seq.length cells = 81 then cells |> Seq.zip squares |> Map.ofSeq |> Some else None |
||
/// Convert grid to a Map of constraint |
/// Convert grid to a Map of constraint propagated possible values, or return None if a contradiction is detected. |
||
let applyCPS (parsedGrid:Map<_, |
let applyCPS (parsedGrid:Map<_,_>) = |
||
let values = [for s in squares do |
let values = [| for s in squares do digits |] |
||
parsedGrid |
parsedGrid |
||
|> |
|> Seq.filter (fun (KeyValue(_,d)) -> digits |> Seq.contains d) |
||
⚫ | |||
|> all (fun vx (KeyValue(s,d)) -> assign vx s d) (Some values) |
|||
/// Calculate string centre for each square - which can contain more than 1 digit when debugging |
/// Calculate string centre for each square - which can contain more than 1 digit when debugging |
||
Line 4,234: | Line 4,234: | ||
/// Display these values as a 2-D grid. Used for debugging |
/// Display these values as a 2-D grid. Used for debugging |
||
let prettyPrint (values: |
let prettyPrint (values:int[][]) = |
||
let asString = Seq.map string >> String.concat "" |
let asString = Seq.map string >> String.concat "" |
||
let width = 1 + ([for s in squares do Seq.length values[s]] |> List.max) |
let width = 1 + ([for s in squares do Seq.length values[s]] |> List.max) |
||
Line 4,240: | Line 4,240: | ||
[for r in rows do |
[for r in rows do |
||
for c in cols do |
for c in cols do |
||
sprintf "%s%s" (centre (asString values[key r c]) |
sprintf "%s%s" (centre (asString values[key r c]) width) (if List.contains c [3;6] then "|" else "") |
||
sprintf "\n%s"(if List.contains r [3;6] then line else "") ] |
sprintf "\n%s"(if List.contains r [3;6] then line else "") ] |
||
|> String.concat "" |
|> String.concat "" |
||
Line 4,246: | Line 4,246: | ||
/// Outputs single line puzzle with 0 as empty squares |
/// Outputs single line puzzle with 0 as empty squares |
||
let asString values = values |> Map.toSeq |> Seq.map (snd>>string) |> String.concat "" |
let asString values = values |> Map.toSeq |> Seq.map (snd>>string) |> String.concat "" |
||
let copy values = values |> Array.map Array.copy |
|||
/// Using depth-first search and propagation, try all possible values. |
/// Using depth-first search and propagation, try all possible values. |
||
let rec search (values: |
let rec search (values:int[][])= |
||
[for s in squares do if Seq.length values[s] > 1 then Seq.length values[s] ,s] |
[for s in squares do if Seq.length values[s] > 1 then Seq.length values[s] ,s] |
||
|> function |
|> function |
||
| [] -> Some values // Solved! |
| [] -> Some values // Solved! |
||
| list -> // tryPick ~ Norvig's `some` |
| list -> // tryPick ~ Norvig's `some` |
||
list |> List.minBy fst |
list |> List.minBy fst |
||
|> fun (_,s) -> values[s] |> Seq.tryPick (fun d -> assign (copy values) s d |> (Option.bind search)) |
|||
let run n g f = parseGrid >> function None -> n | Some m -> f m |> g |
let run n g f = parseGrid >> function None -> n | Some m -> f m |> g |
||
Line 4,259: | Line 4,262: | ||
let solveNoSearch: string -> string = solver applyCPS |
let solveNoSearch: string -> string = solver applyCPS |
||
let solveWithSearch: string -> string = solver (applyCPS >> (Option.bind search)) |
let solveWithSearch: string -> string = solver (applyCPS >> (Option.bind search)) |
||
let solveWithSearchToMapOnly:string -> |
let solveWithSearchToMapOnly:string -> int[][] option = run None id (applyCPS >> (Option.bind search)) </lang> |
||
'''Usage'''<lang fsharp>open System |
'''Usage'''<lang fsharp>open System |
||
open |
open SudokuCPSArray |
||
open System.Diagnostics |
open System.Diagnostics |
||
open System.IO |
open System.IO |
||
Line 4,277: | Line 4,280: | ||
printfn "Try again with search:" |
printfn "Try again with search:" |
||
simple |> run "Parse Error" prettyPrint (Map.toSeq >> Seq.map (fun (k,v) -> k , string v) >> Map.ofSeq) |> printfn "%s" |
|||
simple |> solveWithSearch |> printfn "%s" |
simple |> solveWithSearch |> printfn "%s" |
||
Line 4,303: | Line 4,305: | ||
if result |> Seq.forall Option.isSome then |
if result |> Seq.forall Option.isSome then |
||
let total = watch.ElapsedMilliseconds |
let total = watch.ElapsedMilliseconds |
||
let avg = (float total) /(float result.Length) |
|||
printfn $"\nPuzzles:{result.Length}, Total:%.2f{((float)total)/1000.0} s, Average:%.2f{avg} ms" |
|||
else |
else |
||
printfn "Some sudoku17 puzzles failed" |
printfn "Some sudoku17 puzzles failed" |
||
Line 4,336: | Line 4,339: | ||
Try again with search: |
Try again with search: |
||
4 0 0 |0 0 0 |8 0 5 |
|||
0 3 0 |0 0 0 |0 0 0 |
|||
0 0 0 |7 0 0 |0 0 0 |
|||
------+------+------ |
|||
0 2 0 |0 0 0 |0 6 0 |
|||
0 0 0 |0 8 0 |4 0 0 |
|||
0 0 0 |0 1 0 |0 0 0 |
|||
------+------+------ |
|||
0 0 0 |6 0 3 |0 7 0 |
|||
5 0 0 |2 0 0 |0 0 0 |
|||
1 0 4 |0 0 0 |0 0 0 |
|||
4 1 7 |3 6 9 |8 2 5 |
4 1 7 |3 6 9 |8 2 5 |
||
6 3 2 |1 5 8 |9 4 7 |
6 3 2 |1 5 8 |9 4 7 |
||
Line 4,373: | Line 4,364: | ||
5 9 8 |7 3 6 |2 4 1 |
5 9 8 |7 3 6 |2 4 1 |
||
Elapsed milliseconds = |
Elapsed milliseconds = 8 ms |
||
All puzzles in sudoku17 |
All puzzles in sudoku17 |
||
Puzzles:49151, Total: |
Puzzles:49151, Total:80.99 s, Average:1.65 ms</pre> |
||
===SLPsolve=== |
===SLPsolve=== |