Update of blog post

This commit is contained in:
Sacha Ligthert 2025-01-21 19:06:24 +01:00
parent 0fe5cf0883
commit 49f79b698e

View File

@ -1,15 +1,18 @@
---
title: "Exploration, fun, and process cycles of Sudoku"
date: 2025-01-20T23:33:06+01:00
draft: true
draft: false
---
# The idea
I like to play games, or play with puzzles. Even better, if I can automate solving 1-off single puzzles like Sudoku puzzles I can write an algorithm to solve them and I will never have to play them ever again.
I like to play games, and I to solve puzzles. But, if I can create a puzzle sover for puzzles like Sudoku, I can write an algorithm to solve them once and for all, and I will never have to play them ever again.
[Sudoku puzzles](https://en.wikipedia.org/wiki/Sudoku) have been around for a while and with them [algorithms to solve them](https://en.wikipedia.org/wiki/Sudoku_solving_algorithms). Most of them revolve around throwing random numbers against and see what sticks, intelligent guesswork and finally coming to a solution. I wanted to take a different approach: brute-force all possible solutions, store them in a database. And when I want to have a sudoku puzzle solved I just query the database and it returns all possible solutions.
This idea has been in the back of my mind for close to a decade and late last year I decided to take a shot at it. I dusted off my trusty Go language skills as I a) wanted to learn the language a bit better and b) wanted to use Go routines to easily (ab)use all my CPU cores in this new quest of mine, and finally c) I am terribad at math, so I am working with the tools I have.
This idea has been in the back of my mind for close to a decade and late last year during a week off I decided to take a shot at it. I dusted off my trusty Go language skills as I a) wanted to learn the language a bit better and b) wanted to use Go routines to easily (ab)use all my CPU cores in this new quest of mine, and finally c) I am terribad at math, so I am working with the tools I have got.
(...and this looked like something fun to do. 😏)
# Lay of the land
Classic Sudoku puzzles have 9 blocks in a 3x3 grid with each block containing all the digits from 1 to 9, each block consistent out of 3x3 digits. The puzzle setter provides a partially complete grid and its up to you to solve them, which usually have only a single solution.
@ -18,15 +21,15 @@ Example puzzle:
![Example Sudoku Puzzle](/static/Sudoku_Puzzle_by_L2G-20050714_standardized_layout.svg.png)
( _Honestly stolen from Wikipedia._ )
( _Image honestly stolen from Wikipedia._ )
The first step is to come up with all these unique blocks. As these are the puzzle pieces I need to work with. What I did was the following:
1. Iterate from the lowest possible number (`123456789`) to the highest possible number (`987654321`).
2. Check if all the digits were present once
3. If the block was valid, then print or store it
3. If the block was valid, store it somewhere
This resulted into [this file](https://gitea.ligthert.net/golang/sudoku-funpark/src/branch/trunk/blocks.csv). [Load the file](https://gitea.ligthert.net/golang/sudoku-funpark/src/branch/trunk/solver/blocks.go#L11-L34) into Go as a slice of ints and we can work with that from here on out. (This is faster than adding 363k lines to your source code and keep adding them to a slice, because after 20 minutes of compiling it still wasn't finished and I stopped it. So loading in a CSV was faster)
This resulted into [this file](https://gitea.ligthert.net/golang/sudoku-funpark/src/branch/trunk/blocks.csv). [Load the file](https://gitea.ligthert.net/golang/sudoku-funpark/src/branch/trunk/solver/blocks.go#L11-L34) into Go as a slice of ints and we can work with that from here on out. (This is faster than adding 363 thousand lines to your source code and keep adding them to a slice one by one, because after 20 minutes of compiling it still wasn't finished and I stopped it. So loading in a CSV was faster.)
Inspecting the file it resulted into `362880` possible blocks. It was only later that I noticed that this was the same as `9!` (9 factorial aka `9 * 8 * 7 * 6 * 5 * 4 * 3 * 2 * 1`). It wasn't entirely a surprise that the number 9 returned in a mathy game like Sudoku about 9 digits in a grid of 9 blocks. As far as I can tell this was the last time I encountered something 9 in the maths later on down the line.
@ -48,7 +51,7 @@ This become a mess as I had to ensure that:
2. Every horizontal line was unique
3. Every vertical line was unique
The code for this became a lengthy headache, as I need to work with multi-dimensional array and make sure elements 3, 4, and 5 do not mess to much with other. And I ended up with some kind of mapping ensuring that there was no overlap. It was tedious to design, create, test, and very heavy on the processor to properly analyse everything.
The code for this became a lengthy headache, as I need to work with multi-dimensional array and make sure elements 3, 4, and 5 do not conflict with other elements in other blocks. And I ended up with some kind of mapping structure ensuring that there was no overlap. It was tedious to design, create, test, and very heavy on the processor to properly analyse everything.
It was at this point I had an epiphany and realized that:
1. All blocks have to be unique
@ -65,7 +68,7 @@ So, instead of comparing 3x3 grids, why not use rows to populate the puzzle?
There are benefits to this approach:
1. It would not impact the end result as 3 unique lines that do not violate the constraints of the puzzle will result in 3 unique and valid blocks in a 1x3 row.
2. It is easier to compare rows than 3x3 digits and its adjecent 3x3 digits.
2. It is easier to compare the columns of all rows than the 3x3 digits and its adjecent 3x3 digits.
3. It saves on precious process cycles, which ultimately would speed up the entire process.
@ -87,10 +90,16 @@ I found some random and easy Sudoku puzzle and put this into my code:
I substituted empty entries with a `0`, and used this to find possible substitutions with the remaining numbers.
I will take `row1` as an example for the next bit:
> row1 := "769104802"
```
row1 := "769104802"
```
Replacing the null values with the remaining number I wrote an algorithm that would find all the possible solutions that could work for row one. Missing only two digits this would leave me with two compatible entries:
> 769134852
> 769154832
```
769134852
769154832
```
I put them into a slice and moved on to the next row. And repeated this until all the 9 rows had a slice with possible compatible blocks (row1s, row2s, row3s, etc, etc)
The next step was comparing all the 9 slices, compare every entry, and validate every possible solution. This resulted into [a nesting 9 levels deep](https://gitea.ligthert.net/golang/sudoku-funpark/src/commit/16de7dda97747812eb99ef14088656e5f413b090/solver/processing.go#L45-L71):
@ -162,5 +171,5 @@ It was somewhere at this stage I started looking into other solutions and number
And the latter starts adding up when it comes to storage requiring me to have at least 540 zettabytes to store the solutions as efficiently as possible (a string 81 bytes). Let alone the upfront costs and infra required to hosts such a database.
# Conclusion
# I give up...
I tried, I bit of more than I could chew, I learned a lot, it was fun. I can sleep well knowning I did my best, challanged myself, and can cross something of my todo list that has been living rent free in the back of my head for the better part of a decade.