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 |
||
module SudokuCPS |
module SudokuCPS |
||
// Throughout this program we have: |
|||
// r is a row, e.g. "A" |
|||
// c is a column, e.g. "3" |
|||
// s is a square, e.g. "A3" |
|||
// d is a digit, e.g. 9 |
|||
// u is a unit, e.g. ["A1","B1","C1","D1","E1","F1","G1","H1","I1"] |
|||
// g is a grid, e.g. 81 non-blank chars, e.g. starting with ".18...7... |
|||
// values is a dict of possible values, e.g. {"A1":seq{1;2;3;4;8;9}, "A2":seq{8}, ...} |
|||
open System |
open System |
||
// helpers |
// helpers |
||
let tuple2 a b = a,b |
let tuple2 a b = a,b |
||
let (>>=) f g = |
let (>>=) f g = Option.bind g f |
||
/// folds folder returning Some on |
/// folds folder returning Some on completion or returns None if not |
||
let all folder state = |
let rec all folder state = function |
||
| [] -> state |
|||
| hd::rest -> state >>= (fun st -> folder st hd) |> (fun st1 -> all folder st1 rest) |
|||
/// "A1" to "I9" squares as key in values dictionary |
/// "A1" to "I9" squares as key in values dictionary |
||
Line 4,189: | Line 4,182: | ||
/// Eliminate d from values[s] and propagate when values = 1. |
/// Eliminate d from values[s] and propagate when values = 1. |
||
/// Return Some values, except return None if a contradiction is detected. |
/// Return Some values, except return None if a contradiction is detected. |
||
let rec eliminate (values:Map< |
let rec eliminate (values:Map<string,int[]>) (s:string) d = |
||
let peerElim (vx:Map< |
let peerElim (vx:Map<string,int[]>) = |
||
match Seq.length vx[s] with |
match Seq.length vx[s] with // (1) If a square s is reduced to one value d, then eliminate d from the peers. |
||
| 0 -> |
| 0 -> None // removed last value |
||
| 1 -> peers[s] |> all (fun st s2 -> eliminate st s2 (vx[s] |> Seq.head) ) ( |
| 1 -> peers[s] |> List.ofArray |> all (fun st s2 -> eliminate st s2 (vx[s] |> Seq.head) ) (Some vx) |
||
| _ -> |
| _ -> Some vx |
||
let unitsElim vx = |
let unitsElim vx = |
||
⚫ | |||
⚫ | |||
|> all (fun (st:Map< |
|> List.ofArray |> all (fun (st:Map<string,int[]>) u -> |
||
let dKeys = [for s in u do if st[s] |> Seq.contains d then s] |
let dKeys = [for s in u do if st[s] |> Seq.contains d then s] |
||
match Seq.length dKeys with |
match Seq.length dKeys with |
||
| 0 -> |
| 0 -> None |
||
| 1 -> assign st (Seq.head dKeys) d |
| 1 -> assign st (Seq.head dKeys) d |
||
| _ -> |
| _ -> Some st) (Some vx) |
||
match values[s] |> Seq.contains d with |
match values[s] |> Seq.contains d with |
||
| false -> |
| false -> Some values // Already eliminated, nothing to do |
||
⚫ | |||
| true -> // (1) If a square s is reduced to one value d, then eliminate d from the peers. |
|||
let values2 = values |> Map.change s (Option.map (fun dx -> dx |> Array.filter ((<>)d))) |
let values2 = values |> Map.change s (Option.map (fun dx -> dx |> Array.filter ((<>)d))) |
||
peerElim values2 |
peerElim values2 |
||
⚫ | |||
⚫ | |||
| |
| None -> None // from peers |
||
| |
| Some values3 -> unitsElim values3 |
||
/// Eliminate all the other values (except d) from values[s] and propagate. |
/// Eliminate all the other values (except d) from values[s] and propagate. |
||
Line 4,219: | Line 4,212: | ||
values[s] |
values[s] |
||
|> Seq.filter ((<>)d) |
|> Seq.filter ((<>)d) |
||
|> all (fun st d2 -> eliminate st s d2) ( |
|> List.ofSeq |> all (fun st d2 -> eliminate st s d2) (Some values) |
||
/// 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. |
||
let parseGrid grid = |
let parseGrid grid = |
||
let cells = [for c in grid do if valid |> Seq.contains c then if empty |> Seq.contains c then 0 else ((string>>int) c)] |
let cells = [for c in grid do if valid |> Seq.contains c then if empty |> Seq.contains c then 0 else ((string>>int) c)] |
||
if Seq.length cells = 81 then cells |> Seq.zip squares |> Map.ofSeq |> |
if Seq.length cells = 81 then cells |> Seq.zip squares |> Map.ofSeq |> Some else None |
||
/// Convert grid to a Map of constraint popagated possible values, or return None if a contradiction is detected. |
/// Convert grid to a Map of constraint popagated possible values, or return None if a contradiction is detected. |
||
let applyCPS |
let applyCPS (parsedGrid:Map<string,int>) = |
||
let values = [for s in squares do s, digits]|> Map.ofList |
let values = [for s in squares do s, digits]|> Map.ofList |
||
parsedGrid |
|||
> |
|> (Seq.filter (fun sd -> digits |> Seq.contains sd.Value) |
||
>> all (fun vx sd -> assign vx sd.Key sd.Value) ( |
>> List.ofSeq >> all (fun vx sd -> assign vx sd.Key sd.Value) (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,242: | Line 4,235: | ||
/// Display these values as a 2-D grid. Used for debugging |
/// Display these values as a 2-D grid. Used for debugging |
||
let prettyPrint ( |
let prettyPrint (values:Map<_,_>) = |
||
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) |
|||
match valuesOpt with |
|||
let line = sprintf "%s\n" ((String('-',width*3) |> Seq.replicate 3) |> String.concat "+") |
|||
| Error error -> error |
|||
[for r in rows do |
|||
for c in cols do |
|||
sprintf "%s%s" (centre (asString values[key r c]) width) (if "36".Contains c then "|" else "") |
|||
sprintf "\n%s"(if "CF".Contains r then line else "") ] |
|||
⚫ | |||
for c in cols do |
|||
sprintf "%s%s" (centre (asString values[key r c]) width) (if "36".Contains c then "|" else "") |
|||
sprintf "\n%s"(if "CF".Contains r then line else "") ] |
|||
⚫ | |||
/// Outputs single line puzzle with 0 as empty squares |
/// Outputs single line puzzle with 0 as empty squares |
||
⚫ | |||
let asString = function |
|||
⚫ | |||
| Error error -> error |
|||
/// Using depth-first search and propagation, try all possible values. |
/// Using depth-first search and propagation, try all possible values. |
||
let rec search (values:Map<_,_>) |
let rec search (values:Map<_,_>)= |
||
let rec some = |
let rec some sx = |
||
values[Seq.head sx] |
|||
⚫ | |||
⚫ | |||
values[s] |
|||
⚫ | |||
|> function |
|> function |
||
| Some seqx when Seq.isEmpty seqx -> some sx |
| Some seqx when Seq.isEmpty seqx -> some (Seq.tail sx) |
||
| Some seq -> |
| Some seq -> Some seq |
||
| _ -> |
| _ -> None |
||
// Choose the unfilled square(s) s with the fewest possibilities |
// Choose the unfilled square(s) s with the fewest possibilities |
||
[for s in squares do if Seq.length values[s] > 1 then Seq.length values[s] ,s] |
|||
|> function |
|> function |
||
| |
| [] -> Some values // solved! |
||
| |
| list -> list |> List.sortBy fst |> id |> List.map snd |> id |> some |
||
// Option functions glue/ Command Line Helpers (Could be clearer!) |
|||
/// Solve Sudoku using Constraint Propagation |
|||
let optionFuncToString error applyF stringF = function None -> error | Some m -> applyF m |> stringF |
|||
let solve grid = grid |> applyCPS >>= search |> prettyPrint</lang> |
|||
let solveNoSearch = |
|||
optionFuncToString "Parse Error" applyCPS (Option.fold (fun _ t -> t |> prettyPrint) "No Solution") |
|||
let solve = |
|||
optionFuncToString "Parse Error" (applyCPS >> (Option.bind search)) (Option.fold (fun _ t -> t |> prettyPrint) "No Solution") </lang> |
|||
'''Usage'''<lang fsharp>open System |
'''Usage'''<lang fsharp>open System |
||
open SudokuCPS |
open SudokuCPS |
||
open System.Diagnostics |
|||
open System.IO |
|||
[<EntryPoint>] |
[<EntryPoint>] |
||
let main argv = |
let main argv = |
||
printfn "Easy board solution automatic with constraint propagation" |
printfn "Easy board solution automatic with constraint propagation" |
||
let easy = "..3.2.6..9..3.5..1..18.64....81.29..7.......8..67.82....26.95..8..2.3..9..5.1.3.." |
let easy = "..3.2.6..9..3.5..1..18.64....81.29..7.......8..67.82....26.95..8..2.3..9..5.1.3.." |
||
easy |> |
easy |> parseGrid |> solveNoSearch |> printfn "%s" |
||
printfn "Simple elimination not possible" |
printfn "Simple elimination not possible" |
||
let simple = "4.....8.5.3..........7......2.....6.....8.4......1.......6.3.7.5..2.....1.4......" |
let simple = "4.....8.5.3..........7......2.....6.....8.4......1.......6.3.7.5..2.....1.4......" |
||
simple |> parseGrid |> asString |> printfn "%s |
simple |> parseGrid |> optionFuncToString "Parse Error" id asString |> printfn "%s" |
||
simple |> |
simple |> parseGrid |> solveNoSearch |> printfn "%s" |
||
printfn "Try again with search:" |
printfn "Try again with search:" |
||
simple |> parseGrid |> |
simple |> parseGrid |> optionFuncToString "Parse Error" (Map.toSeq >> Seq.map (fun (k,v) -> k , string v) >> Map.ofSeq) prettyPrint |> printfn "%s" |
||
simple |> solve |> printfn "%s" |
simple |> parseGrid |> solve |> printfn "%s" |
||
let watch = Stopwatch() |
let watch = Stopwatch() |
||
Line 4,302: | Line 4,294: | ||
printfn "Hard" |
printfn "Hard" |
||
watch.Start() |
watch.Start() |
||
hard |> |
hard |> parseGrid |> solve |> printfn "%s" |
||
watch.Stop() |
watch.Stop() |
||
printfn $"Elapsed milliseconds = {watch.ElapsedMilliseconds } ms" |
printfn $"Elapsed milliseconds = {watch.ElapsedMilliseconds } ms" |
||
watch.Reset() |
watch.Reset() |
||
⚫ | |||
let num = 10000 |
|||
⚫ | |||
// http://www.csse.uwa.edu.au/~gordon/sudoku17 |
|||
⚫ | |||
⚫ | |||
watch.Start() |
watch.Start() |
||
let result = puzzles |> |
let result = puzzles |> Array.map (parseGrid >> solve) |
||
watch.Stop() |
watch.Stop() |
||
let total = watch.ElapsedMilliseconds |
let total = watch.ElapsedMilliseconds |
||
printfn $"\nPuzzles:{result.Length}, Total:{total} ms, Average:{((float total) /(float result.Length))} ms" |
|||
printfn " (i7500U @2.7GHz CPU, 16GB Ram)" |
|||
Console.ReadKey() |> ignore |
Console.ReadKey() |> ignore |
||
0 |
0</lang> |
||
⚫ | |||
⚫ | |||
⚫ | |||
4 8 3 |9 2 1 |6 5 7 |
4 8 3 |9 2 1 |6 5 7 |
||
9 6 7 |3 4 5 |8 2 1 |
9 6 7 |3 4 5 |8 2 1 |
||
Line 4,336: | Line 4,324: | ||
Simple elimination not possible |
Simple elimination not possible |
||
400000805030000000000700000020000060000080400000010000000603070500200000104000000 |
400000805030000000000700000020000060000080400000010000000603070500200000104000000 |
||
4 1679 12679 | 139 2369 269 | 8 1239 5 |
4 1679 12679 | 139 2369 269 | 8 1239 5 |
||
26789 3 1256789 | 14589 24569 245689 | 12679 1249 124679 |
26789 3 1256789 | 14589 24569 245689 | 12679 1249 124679 |
||
Line 4,387: | Line 4,374: | ||
5 9 8 |7 3 6 |2 4 1 |
5 9 8 |7 3 6 |2 4 1 |
||
Elapsed milliseconds = |
Elapsed milliseconds = 7 ms |
||
All puzzles in sudoku17 |
|||
Puzzles:10000, Total:46090 ms, Average:4.609 ms (i7500U @2.7GHz CPU, 16GB Ram)</pre> |
|||
Puzzles:49151, Total:184642 ms, Average:3.7566275355537018 ms |
|||
⚫ | |||
===SLPsolve=== |
===SLPsolve=== |