Sudoku: Difference between revisions
Content added Content deleted
Line 4,102: | Line 4,102: | ||
<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 = Result.bind g f |
||
/// folds folder returning Some on |
/// folds folder returning Some on compleition or returns None if not |
||
let all folder state = Seq.fold (fun acc cur -> acc >>= (fun st -> folder st cur)) state |
let all folder state = Seq.fold (fun acc cur -> acc >>= (fun st -> folder st cur)) state |
||
Line 4,117: | Line 4,126: | ||
// constants |
// constants |
||
let digits = |
let digits = [|1..9|] |
||
let rows = "ABCDEFGHI" |
let rows = "ABCDEFGHI" |
||
let cols = "123456789" |
let cols = "123456789" |
||
Line 4,142: | Line 4,151: | ||
/// 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<_,int[]>) (s:string) d = |
||
let peerElim (vx:Map<_,_>) = |
|||
match Seq.length vx[s] with |
|||
⚫ | |||
⚫ | |||
⚫ | |||
⚫ | |||
let unitsElim vx = |
|||
⚫ | |||
|> all (fun (st:Map<_,_>) u -> |
|||
let dKeys = [for s in u do if st[s] |> Seq.contains d then s] |
|||
match Seq.length dKeys with |
|||
| 0 -> Error "units contradiction" |
|||
| 1 -> assign st (Seq.head dKeys) d |
|||
| _ -> Ok st) (Ok vx) |
|||
match values[s] |> Seq.contains d with |
match values[s] |> Seq.contains d with |
||
| false -> |
| false -> Ok values // Already eliminated, nothing to do |
||
⚫ | |||
| true -> |
|||
let values2 = values |> Map.change s (Option.map (fun dx -> dx |> |
let values2 = values |> Map.change s (Option.map (fun dx -> dx |> Array.filter ((<>)d))) |
||
peerElim values2 |
|||
⚫ | |||
|> function // (2) If a unit u is reduced to only one place for a value d, then put it there. |
|||
⚫ | |||
| Error error -> Error error // from peers |
|||
⚫ | |||
| |
| Ok 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. |
||
/// Return Some values, except None if a contradiction is detected. |
/// Return Some values, except None if a contradiction is detected. |
||
and assign (values:Map<_,_>) s d = |
and assign (values:Map<_,_>) (s:string) d = |
||
values[s] |
values[s] |
||
|> Seq.filter ((<>)d) |
|> Seq.filter ((<>)d) |
||
|> all (fun st d2 -> eliminate st s d2) ( |
|> all (fun st d2 -> eliminate st s d2) (Ok 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 |> Ok else Error $"parseGrid length ={Seq.length cells}" |
||
/// 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. |
||
Line 4,169: | Line 4,193: | ||
parseGrid grid |
parseGrid grid |
||
>>= (Seq.filter (fun sd -> digits |> Seq.contains sd.Value) |
>>= (Seq.filter (fun sd -> digits |> Seq.contains sd.Value) |
||
>> all (fun vx sd -> assign vx sd.Key sd.Value) ( |
>> all (fun vx sd -> assign vx sd.Key sd.Value) (Ok 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,179: | Line 4,203: | ||
sprintf "%s%s%s" (String(' ',half)) s (String(' ', n - half)) |
sprintf "%s%s%s" (String(' ',half)) s (String(' ', n - half)) |
||
let error = "Unable to parse input or solve puzzle" // TODO: Update to Result |
|||
/// Display these values as a 2-D grid. Used for debugging |
/// Display these values as a 2-D grid. Used for debugging |
||
let prettyPrint (valuesOpt:Map<_,_> |
let prettyPrint (valuesOpt:Result<Map<_,_>,_>) = |
||
let asString = Seq.map string >> String.concat "" |
let asString = Seq.map string >> String.concat "" |
||
match valuesOpt with |
match valuesOpt with |
||
| |
| Error error -> error |
||
| |
| Ok values -> |
||
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) |
||
let line = sprintf "%s\n" ((String('-',width*3) |> Seq.replicate 3) |> String.concat "+") |
let line = sprintf "%s\n" ((String('-',width*3) |> Seq.replicate 3) |> String.concat "+") |
||
Line 4,196: | Line 4,219: | ||
/// Outputs single line puzzle with 0 as empty squares |
/// Outputs single line puzzle with 0 as empty squares |
||
let asString = function |
let asString = function |
||
| |
| Ok values -> values |> Map.toSeq |> Seq.map (snd>>string) |> String.concat "" |
||
| |
| 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<_,_>): Result<Map<_,_>,_> = |
||
let rec some = function |
|||
if values |> Map.toSeq |> Seq.forall (fun kvp -> Seq.length (snd kvp) = 1) then Some values |
|||
| [] -> Error "No solution" |
|||
else |
|||
| s::sx -> |
|||
values[s] |
|||
|> Seq.tryPick (fun d -> assign values s d >>= search |> function Error _ -> None | Ok seq -> Some seq) |
|||
⚫ | |||
|> function |
|||
| Some seqx when Seq.isEmpty seqx -> some sx |
|||
| Some seq -> Ok seq |
|||
| _ -> Error "Another error" |
|||
⚫ | |||
| filled -> filled |
|||
⚫ | |||
⚫ | |||
⚫ | |||
⚫ | |||
| seqx when Seq.isEmpty seqx -> Ok values // solved! |
|||
|> List.groupBy fst |> List.min |> snd |> List.map snd |
| seqx -> seqx |> List.groupBy fst |> List.min |> snd |> List.map snd |> some |
||
⚫ | |||
/// Solve Sudoku using Constraint Propagation |
/// Solve Sudoku using Constraint Propagation |
||
Line 4,233: | Line 4,256: | ||
printfn "Try again with search:" |
printfn "Try again with search:" |
||
simple |> parseGrid |> |
simple |> parseGrid |> Result.map (Map.toSeq >> Seq.map (fun (k,v) -> k , string v) >> Map.ofSeq) |> prettyPrint |> printfn "%s" |
||
simple |> |
simple |> solve |> printfn "%s" |
||
let watch = Stopwatch() |
|||
let hard = "85...24..72......9..4.........1.7..23.5...9...4...........8..7..17..........36.4." |
let hard = "85...24..72......9..4.........1.7..23.5...9...4...........8..7..17..........36.4." |
||
printfn "Hard" |
printfn "Hard" |
||
watch.Start() |
|||
hard |> applyCPS >>= search |> prettyPrint |> printfn "%s" |
hard |> applyCPS >>= search |> prettyPrint |> printfn "%s" |
||
watch.Stop() |
|||
printfn $"Elapsed milliseconds = {watch.ElapsedMilliseconds } ms" |
|||
watch.Reset() |
|||
let num = 10000 |
|||
// http://www.csse.uwa.edu.au/~gordon/sudoku17 |
|||
printfn $"First {num} puzzles in sudoku17" |
|||
let puzzles = File.ReadLines(@"sudoku17.txt") |> Seq.take num |> List.ofSeq |
|||
watch.Start() |
|||
let result = puzzles |> List.map (fun p -> p |> applyCPS >>= search |> asString) |
|||
watch.Stop() |
|||
let total = watch.ElapsedMilliseconds |
|||
printfn $"Puzzles:{num}, Total:{total} ms, Average:{((float total) /(float num))} ms" |
|||
printfn "Press any key to exit" |
printfn "Press any key to exit" |
||
Line 4,259: | Line 4,298: | ||
Simple elimination not possible |
Simple elimination not possible |
||
400000805030000000000700000020000060000080400000010000000603070500200000104000000 |
400000805030000000000700000020000060000080400000010000000603070500200000104000000 |
||
4 1679 12679 | 139 2369 1269 | 8 1239 5 |
|||
4 1679 12679 | 139 2369 269 | 8 1239 5 |
|||
26789 3 1256789 | 14589 24569 245689 | 12679 1249 124679 |
|||
2689 15689 125689 | 7 234569 245689 | 12369 12349 123469 |
|||
------------------------+------------------------+------------------------ |
------------------------+------------------------+------------------------ |
||
3789 2 |
3789 2 15789 | 3459 34579 4579 | 13579 6 13789 |
||
3679 15679 |
3679 15679 15679 | 359 8 25679 | 4 12359 12379 |
||
36789 |
36789 4 56789 | 359 1 25679 | 23579 23589 23789 |
||
------------------------+------------------------+------------------------ |
------------------------+------------------------+------------------------ |
||
289 89 289 | 6 459 3 | 1259 7 12489 |
289 89 289 | 6 459 3 | 1259 7 12489 |
||
5 6789 |
5 6789 3 | 2 479 1 | 69 489 4689 |
||
1 6789 4 | 589 579 5789 | 23569 23589 23689 |
1 6789 4 | 589 579 5789 | 23569 23589 23689 |
||
Try again with search: |
Try again with search: |
||
4 0 0 |0 0 0 |8 0 5 |
4 0 0 |0 0 0 |8 0 5 |
||
Line 4,308: | Line 4,349: | ||
5 9 8 |7 3 6 |2 4 1 |
5 9 8 |7 3 6 |2 4 1 |
||
Elapsed milliseconds = 28 ms |
|||
First 10000 puzzles in sudoku17 |
|||
Puzzles:10000, Total:67072 ms, Average:6.7072 ms |
|||
Press any key to exit</pre> |
Press any key to exit</pre> |
||