Me writing my sudoku solver
Table of Contents
Encountered a stuck solution problem
1. Introduction
1.1. Layout and the structure of this "blog"
1.2. Why I wanted to write this Sudoku solver
I have tried writing a Sudoku solver before, one completed using a very simple brute-force depth-first backtrack searcher in C++ for an university assignment, another using a constraint based depth-first searcher in Clojure, in common lisp, etc, in-completed.
The simple brute-force was elegant, efficient and does the job, however, I have thought about how I myself solves the sudoku when I see it right in front of me, I want to make a program that solve the sudoku that way. This is the main motivation with this program, and there are two main goals behind this attempt:
- to see to myself finishing a project from start to finish
- to challenge myself, to complete something that I hadn't completed individually yet and with sudoku it isn't something too difficult that I would give up mid-way like many times before.
2. Introduction to Sudoku
Let's go through the basics of a sudoku, i.e. what is a sudoku.
The rule and the goal of sudoku is as follows: fill in the 9 by 9 grid with numbers from 1 to 9 in each columns and rows such that all columns, rows, and sub-grid contain a number from 1 to 9 uniquely. Here are some properties of a typical 9 by 9 sudoku:
- 9 rows, each labelled with 'i's
- 9 columns, each labelled with 'j's
- and 9 sub-grid as seen in the ascii diagram below labelled from A to I
jjj jjj jjj ――――――――――――― i │...│...│...│ i │.A.│.B.│.C.│ i │...│...│...│ ――――――――――――― i │...│...│...│ i │.D.│.E.│.F.│ i │...│...│...│ ――――――――――――― i │...│...│...│ i │.G.│.H.│.I.│ i │...│...│...│ ―――――――――――――
For example, the following are "illegal":
――――――――――――― │251│...│..1│ │349│...│..2│ │286│...│..3│ ――――――――――――― │...│...│..4│ │...│...│..7│ │...│...│..8│ ――――――――――――― │...│...│..6│ │...│...│..5│ │123│455│789│ ―――――――――――――
3. Algorithm approach
Solving Sudoku is a solved and known problem, there are many widely available algorithms for solving Sudoku. For example, in as part of my University programming course, the most common approach was to use back-tracking. This time I want to experiment and figure out an approach.
Now this is a random sudoku I copied from one of the sudoku sites, which I googled for.
――――――――――――― │871│345│926│ │349│726│851│ │256│891│473│ ――――――――――――― │132│479│685│ │598│162│734│ │764│538│219│ ――――――――――――― │915│687│342│ │427│913│568│ │683│254│197│ ―――――――――――――
I solved sudoku before, this time I want to come up with an algorithm that almost approach a sudoku like how I approached it as a human. So, how do I usually solve a sudoku problem?
First, I would look through the entire sudoku itself 9 by 9, simply with a glance, it's done with a combination of subconscious and the conscious! Then I would start to look at a position and start to reason what numbers can be placed at this position. For instance, at (0,0) there are 3,4,5 and 2 on the same row then column-wise, there are 3,2,1,5 and 9 reading down the first column in order. Combining the two gives a set of 1,2,3,4,5,,,9 with 6,7,8 missing. Ouu, but hang on I've missed out on the sub-grid A, which gives another 6. Finally that leaves the available candidates as either 7 or 8.
Actually we can narrow one step further, by taking advantage of the information from the other sub-grids, for instance, 7s in B and C have occupied the bottom two lanes, then the 7 must be in the top row for grid A. And then the same can be done for sub-grid D and G (which are the two sub-grids below A) etc.
Ok the above is what my brain does by doing a series of reasoning to eliminate and find out the right candidate or candidates, in this case its plural because there are multiple choices. By repeating this methodology, eventually there will be one position that will only have one possible candidate and the domino fall from there and after X number of steps the sudoku is solved.
3.1. How to translate into lisp code
First of all, a data-structure is needed to hold the representation of a sudoku board in memory. In this case it's trivia to select a 2d array. In other more complicated computer problems, choosing and implementing the right data-structure will often help facilitate progress rather than hinder it, and choosing poorly will cause hindrance.
Anyhow, with a 2d array, the rows of a sudoku can be accessed with O(1) time. Accessing the columns is requires a bit more work. Although these matters less compared to the algorithm as because accessors for rows and columns are effectively function calls and the cost of the function in this case being row is both faster and cheaper can be optimized later on.
Moving onto the algorithm side, the program will start looking at one position at a time. For a position (i,j) it will look into the i-th row and j-th column and starts filtering down the possible candidates from there.
Normally, I would use a set to represent the combination of unique numbers from the row and column, for instance, in table of numbers we see what the set of numbers are existing in row0 then in col0 (column 0), in the third table row it shows what's the combined set of row 0 and column 0. The last table row showing the missing numbers from the combined set.
| PLACE | SET OF NUMBERS |
|---|---|
| row0 | (1 2 3 4 5 6 7 8 9) |
| col0 | (1 2 3 4 5 6 7 8 9) |
| row0 + col0 | (1 2 3 4 5 6 7 8 9) |
| what's missing | NIL |
Set is a good tool for holding the data I need. However, I wanted to try something different this time, with compactness in mind. The problem space here is, a way to encode the state of a row, column and region (sub-grid), given that each number from 1 to 9 are either present or missing. After some time I realized bit-mask is the right tool for this, what's better is that, there are boolean operation that helps combining two encodings. I settled with 1 as missing and 0 as present because of overlooked the operation "binary nor" and "binary and" only works with the chosen schema.
(
NOTE: binary mask works different in Common Lisp, they are called bit-vectors, essentially a vector or 1d array that holds only bits (either 1 or 0)
They have a literal syntax of for instance
#*1111, which you may think of as0b1111in equivalence of binary number system, then going forward with the examples to illustrate the different between common-lisp "binary mask" (bit arrays) is that for instance,#*1100is the same as0b0011not0b1100as you might have thought. In other words, the bit array are counting from the left most, or that the left most digit is the least significant bit, as contrary to the binary number system where0b001has a 1 in its least significant bit since it counts from the right most.now with a simple table at common lisp bit array table to finish this all off.
)
| Bit array | in Binary | in Decimal |
|---|---|---|
| #*1100 | 0b1100 | 12 |
| #*0011 | 0b0011 | 3 |
Coming back to the sudoku at hand, let's say there is an encoding of #*111000000 this would represent 1,2,3 are missing and the rest are present for any particular region, row, column or all three combined.
3.2. More on encoding
| PLACE | SET OF NUMBERS | ENCODING (inverted) |
|---|---|---|
| row0 | (1 2 3 4 5 7 8) | #*111110110 |
| col6 | (2 3 4 7) | #*011100100 |
| first region | (1 2 4 7 8) | #*110100110 |
| row0 + col0 | (1 2 3 4 5 7 8) | #*111110110 |
let's say i is 0 and j is 6 and we get the following encoding:
The encoding translate back into the possible candidates for this position which are (counted from the LSB as 1):
| 6, | 9 |
My solver started to step through the sudoku and filling in the numbers in their respective positions. But then it halted at step 10
4. the second version
After looking carefully at the final state of the sudoku puzzle from the currently stuck position, I find out that all the empty spots were having more than 1 possible candidates, this prevented the solver from advancing to the next state.
It didn't took long to find out the solver wasn't taking the advantage of the numbers/information from the other "lanes" (rows and/or columns) to help narrow down the choice.
――――――――――――― │...│345│.2.│ │349│7.6│..1│ │2.6│..1│47.│ ――――――――――――― │1.2│.79│.85│ │59.│...│...│ │.6.│...│219│ ――――――――――――― │91.│..7│.42│ │.27│913│.6.│ │...│...│...│ ―――――――――――――
Let's look at position (5,0) there are may be up to four possible candidates judging by the 1,2,5,6,9 in the grid. However, if we were to take advantage of the 7s in the grid to the right and below, we can see the only possible place is (5,0). This way of reasoning is what the algorithm need to progress further from the stuck state.
5. the third version
5.1. motivation
Stepping through the sudoku puzzle once more and this time we stopped at step 23.
Looking at the message it tells me the solver is unable to find a position with any candidates. Looking at the board right now, I see this is because there are some contradicting numbers placed onto the board.
For instance, the 6 at (0 . 6) should have been a 9 instead. But why did the program decide to put that 6 there. Back to the encodings:
| Name | encoding |
|---|---|
| row | #*000001001 |
| column | #*100011011 |
| region | #*001011001 |
| combined | #*000001001 |
| numbers | 6, 9 |
This tells me the newly added logic that took advantage of the information/numbers from the other lanes has mis-lead the program. Because for (0 . 6) and (0 . 8) either could have put a 6 or 9 down, but due to design of the searcher that simply goes column first then onto the next row meant that (0 . 6) got filled with a 6.
Time to revise the searcher from positional to regional. The searcher should take into account of the impact of a conflicting choices, which tend to happen to spots within the same row, column or region. Instead of computing for one position at a time, the searcher should compute for 3 rows, 3 columns and 1 region.
5.2. progress
I thought to myself the best way to approach this is to start with a regional view, where we will consider the problem for all three rows, three columns and one single region.
From here the new algorithm is as follows:
- Start from one region, typically starts from region X, where X can be anything from 0 to 8
- Within region X, there are Y number of empty positions, and Z number of empty positions that has a mask with only 1 bit asserted
- there are Y number of masks for each of the empty position, where Y satisfy 0 < Y < X.
- From the Y masks there are Z number of masks with only 1 bit asserted
- Finally, those Z masks are translated back into the integer to fill back into the Sudoku board.
With the algorithm detailed above implemented after stepping through the program a couple times the Sudoku board is finally solved
6. Final completed sudoku from the solver
actually solving sudoku isnt' required at the moment
――――――――――――― │871│345│926│ │349│726│851│ │256│891│473│ ――――――――――――― │132│479│685│ │598│162│734│ │764│538│219│ ――――――――――――― │915│687│342│ │427│913│568│ │683│254│197│ ―――――――――――――
(in-package :cl-sudoku.test) (sudoku-solvedp foo)
T