(Note: sudsoln
is only supported in Python 3)
To install sudsoln
, type:
pip install sudsoln
This vignette focuses on how you can initialize Sudoku
puzzle and use methods to solve them. Any other details, such as classes like Candidate
, Array
, Appearance
, or other Sudoku
methods such as .__eq__()
, .group()
, .copy()
and so on, are omitted.
In this document, the following version of sudsoln
is imported with the following alias:
## 0.1.0
Initializing a puzzle
There are two ways to initialize a puzzle:
- use
Sudoku
class directly - use
to_sudoku
function
The difference between the two is that Sudoku
accepts a list of lists in array
argument, whereas to_sudoku
function accepts a string in sudoku_str
argument.
In fact, to_sudoku
is merely a wrapper function around Sudoku
; inside to_sudoku
, sudoku_str
is converted to a list of lists, and this is plugged into array
argument of Sudoku
.
Both Sudoku
and to_sudoku
require three arguments in total:
- For
Sudoku
, they arearray
,empty
, andelements
- For
to_sudoku
, they aresudoku_str
,empty
, andelements
Details regarding these arguments are written in step 2.
Step 0: check if you have a compatible size/length
sudsoln
is designed to handle \(n^2\)-by-\(n^2\) sudoku puzzles, where \(n \geq 2\) is a number of rows/columns in a subarray (or submatrix
, the term used in this package). You need to make sure that a puzzle has a size/length \(n^4\), where \(n \geq 2\) is a natural number. Compatible sizes/lengths include:
- \(16 = 2^4\),
- \(81 = 3^4\),
- \(256 = 4^4\),
- \(625 = 5^4\)
and so forth.
Step 1: try it without any additional arguments
You can first try initializing sudoku puzzle without specifying any additional arguments (i.e. empty
and elements
). Suppose you have the following puzzle:
q1_array = [
['.', '.', '.', '.', '2', '.', '.', '.', '.'],
['8', '3', '.', '7', '1', '4', '.', '9', '6'],
['.', '6', '.', '9', '.', '5', '4', '.', '8'],
['.', '9', '.', '3', '.', '1', '.', '.', '4'],
['.', '1', '.', '4', '.', '2', '.', '.', '7'],
['.', '7', '5', '.', '.', '.', '2', '1', '.'],
['.', '.', '4', '.', '.', '.', '7', '.', '.'],
['.', '.', '.', '5', '.', '7', '.', '.', '.'],
['.', '.', '.', '1', '9', '6', '.', '.', '.']
]
You may use Sudoku
directly as follows:
## Sudoku(
## . . . | . 2 . | . . .
## 8 3 . | 7 1 4 | . 9 6
## . 6 . | 9 . 5 | 4 . 8
## -------------------+-------------------+-------------------
## . 9 . | 3 . 1 | . . 4
## . 1 . | 4 . 2 | . . 7
## . 7 5 | . . . | 2 1 .
## -------------------+-------------------+-------------------
## . . 4 | . . . | 7 . .
## . . . | 5 . 7 | . . .
## . . . | 1 9 6 | . . .
## n: 3
## elements: 1, 2, 3, 4, 5, 6, 7, 8, 9
## empty: .
## )
If you have it in a form of string:
then you should use to_sudoku
function as follows:
## Sudoku(
## . . . | . 2 . | . . .
## 8 3 . | 7 1 4 | . 9 6
## . 6 . | 9 . 5 | 4 . 8
## -------------------+-------------------+-------------------
## . 9 . | 3 . 1 | . . 4
## . 1 . | 4 . 2 | . . 7
## . 7 5 | . . . | 2 1 .
## -------------------+-------------------+-------------------
## . . 4 | . . . | 7 . .
## . . . | 5 . 7 | . . .
## . . . | 1 9 6 | . . .
## n: 3
## elements: 1, 2, 3, 4, 5, 6, 7, 8, 9
## empty: .
## )
Both initializations are successful without specifying empty
and elements
.
Step 2: specify elements
As mentioned earlier, both Sudoku
and to_sudoku
require three arguments in total. Two common arguments are:
empty
: a string that denotes the emptiness ('.'
by default)elements
: a set of entries inarray
/sudoku_str
that is NOTempty
Notes:
- Unlike
empty
,elements
has no default argument. - Usually, if a size of puzzle is 9-by-9 (or 32-by-32), then
elements
is a set of integers from1
to9
. - Every element in
elements
as well asempty
must have length 1 if it is converted into typestr
. For example,10
or14
cannot be an element ofelements
, or'..'
cannot beempty
.
If elements
is not specified, then it guesses elements
from array
or sudoku_str
depending on what you are using to initialize a puzzle. Specifically, Sudoku
thinks any element in array
that is NOT empty
should be a member of elements
. The reason for successful initializations of q1_array
and q1_str
in step 1 is because it has 9 distinct values — integers from 1 to 9 — in the array
that is NOT empty
(i.e. '.'
).
That is, any puzzle that does NOT have the “correct” number of distinct values will not be initialized in step 1. For example, q4_str
, will not be initialized properly and will return ValueError
with the following message:
## 3.2....6....7.81...........58.4............12...1......1....4..6...3........2....
ValueError: Length of the guessed elements is 8, not 9. Either make sure that:
1. every element in the current array contains all of your intended elements at
least once, or; 2. specify elements explicitly, or; 3. there is exactly one
string, and only one, that denotes the emptiness in the array. For example, if
you try to solve a 9-by-9 sudoku whose answer form consists of integers from 1
to 9, either make sure that every integer from 1 to 9 shows up in the current
array at least once, or explicitly specify elements = set([str(i) for i in
range(1, 10)]), or see if the array uses '.' and some other string, like ',' or
' ', to denote the emptiness.
Initialization is not successful because 9
does not exist in q4_str
, so it causes an error that says the number of guessed elements is 8, not 9.
The solution of this problem is to specify elements
as follows:
## Sudoku(
## 3 . 2 | . . . | . 6 .
## . . . | 7 . 8 | 1 . .
## . . . | . . . | . . .
## -------------------+-------------------+-------------------
## 5 8 . | 4 . . | . . .
## . . . | . . . | . 1 2
## . . . | 1 . . | . . .
## -------------------+-------------------+-------------------
## . 1 . | . . . | 4 . .
## 6 . . | . 3 . | . . .
## . . . | . 2 . | . . .
## n: 3
## elements: 1, 2, 3, 4, 5, 6, 7, 8, 9
## empty: .
## )
Notice that I used set([i for i in range(1, 10)])
although the message suggests to use set([str(i) for i in range(1, 10)])
. You can go with either of these as Sudoku
automatically converts every element in elements
into str
before creating an instance of Sudoku
puzzle anyway.
Step 3: specify the correct empty
string
If you don’t like '.'
as empty
in your puzzle, you have to change it BEFORE initializing into a Sudoku
puzzle (I may provide the empty
setter of Sudoku
in future updates). That is, the following won’t work:
## ....2....83.714.96.6.9.54.8.9.3.1..4.1.4.2..7.75...21...4...7.....5.7......196...
KeyError: "empty = ' ' does not exist in the array. Either specify the correct
string denoting the emptiness in the array, or change the string denoting the
emptiness in the array by using sudsoln.change_empty(array, old, new)."
You see this error message because q1_str
uses a period ('.'
) to denote the emptiness. Instead, you have to do the following:
## 2 83 714 96 6 9 54 8 9 3 1 4 1 4 2 7 75 21 4 7 5 7 196
## Sudoku(
## | 2 |
## 8 3 | 7 1 4 | 9 6
## 6 | 9 5 | 4 8
## -------------------+-------------------+-------------------
## 9 | 3 1 | 4
## 1 | 4 2 | 7
## 7 5 | | 2 1
## -------------------+-------------------+-------------------
## 4 | | 7
## | 5 7 |
## | 1 9 6 |
## n: 3
## elements: 1, 2, 3, 4, 5, 6, 7, 8, 9
## empty:
## )
You may do the following in case of q4_str
:
## 3*2****6****7*81***********58*4************12***1******1****4**6***3********2****
## Sudoku(
## 3 * 2 | * * * | * 6 *
## * * * | 7 * 8 | 1 * *
## * * * | * * * | * * *
## -------------------+-------------------+-------------------
## 5 8 * | 4 * * | * * *
## * * * | * * * | * 1 2
## * * * | 1 * * | * * *
## -------------------+-------------------+-------------------
## * 1 * | * * * | 4 * *
## 6 * * | * 3 * | * * *
## * * * | * 2 * | * * *
## n: 3
## elements: 1, 2, 3, 4, 5, 6, 7, 8, 9
## empty: *
## )
Step 4: fix typos
Suppose you are heading towards the subway station commuting to your workplace. Along the way, you grab a newspaper, read two or three articles, criticize journalists on their political orientations, and then get to the end of the newspaper. You see today’s 32-by-32 sudoku puzzle is right there, remembering there is a Python package called sudsoln
you downloaded before, and you decide to give it a try.
You open your laptop/smartphone/tablet, open a Python shell (on Pydroid/Termux/etc in case of smartphone or tablet), and then type in the following:
You then import sudsoln as ss
, and try initialize Sudoku
using to_sudoku
as follows:
But you get the error message:
ValueError: Length of the guessed elements is 10, not 9. Either make sure that:
1. every element in the current array contains all of your intended elements at
least once, or; 2. specify elements explicitly, or; 3. there is exactly one
string, and only one, that denotes the emptiness in the array. For example, if
you try to solve a 9-by-9 sudoku whose answer form consists of integers from 1
to 9, either make sure that every integer from 1 to 9 shows up in the current
array at least once, or explicitly specify elements = set([str(i) for i in
range(1, 10)]), or see if the array uses '.' and some other string, like ',' or
' ', to denote the emptiness.
Ah, you remember this error message. The writer of this package said that you have to specify elements
in this case:
But this time, you are getting a different error message:
ValueError: There exists an element in array that is not a member of
elements: {','}
What does this mean? This means that puzzle
has a string that is neither empty
nor an element of elements
. Specifically, the error message says that ','
is in your puzzle
which is not supposed to be an element of elements
, but not empty
either.
You scroll up the shell to the point where you defined puzzle
, and realize that the 5th string of puzzle
is ','
and not '.'
. You must have made a mistake while typing the puzzle and have not realized it. After all, a comma and a period is right next to each other:
## ','
You also see that the first error message said the “[l]ength of the guessed elements is 10, not 9.” The guessed elements was not 8 but 10 after all.
Now you know the mistake, you fix the typo and then initialize it:
## Sudoku(
## . . 3 | . . . | . 2 .
## . . . | 4 1 6 | . . 5
## 6 . 8 | . . . | . . .
## -------------------+-------------------+-------------------
## . 2 . | 9 . 4 | . 6 .
## . 6 . | . 8 . | . 7 .
## . 3 . | 2 . 7 | . 9 .
## -------------------+-------------------+-------------------
## . . . | . . . | 6 . 4
## 2 . . | 8 4 1 | . . .
## . 5 . | . . . | 9 . .
## n: 3
## elements: 1, 2, 3, 4, 5, 6, 7, 8, 9
## empty: .
## )
After successful initialization, you are ready to use methods to solve the puzzle.
Solving a puzzle
As of version 0.1.0, there are 7 solving methods provided:
Sudoku.solve_globally()
Sudoku.solve_locally()
Sudoku.solve_by_pointing_pairs()
Sudoku.solve_by_hidden_pairs()
Sudoku.solve_logically()
Sudoku.solve_forcefully()
Sudoku.solve()
.solve_globally()
This method accepts no argument, and returns None
.
For each entry, this method looks at the row
, col
umn, and submatrix
it belongs and see if there is only one possible candidate value at that entry. If true, plug the value into that entry, and repeat the process until no new mutation is made to Sudoku
.
For example, take a look at q1_case1
:
## Sudoku(
## . . . | . 2 . | . . .
## 8 3 . | 7 1 4 | . 9 6
## . 6 . | 9 . 5 | 4 . 8
## -------------------+-------------------+-------------------
## . 9 . | 3 . 1 | . . 4
## . 1 . | 4 . 2 | . . 7
## . 7 5 | . . . | 2 1 .
## -------------------+-------------------+-------------------
## . . 4 | . . . | 7 . .
## . . . | 5 . 7 | . . .
## . . . | 1 9 6 | . . .
## n: 3
## elements: 1, 2, 3, 4, 5, 6, 7, 8, 9
## empty: .
## )
See the entry at the 3rd row and 5th column. Notice that 3
is the only possible candidate value at that entry because:
- there are
4
,5
,6
,8
, and9
in the row it belongs - there are
1
and2
in the column it belongs - there is
7
in the submatrix it belongs
And therefore, the only candidate value left at that entry is 3
, indicating it must be the value at that entry. .solve_globally()
does this for every entry in the sudoku array, and goes back to the very first entry after it goes through the entire array. The method stops if the process does not mutate Sudoku
puzzle anymore.
q1_case1
is actually a very easy sudoku puzzle and only requires .solve_globally()
to be solved entirely:
from datetime import datetime as dt
start = dt.now(); q1_case1.solve_globally(); end = dt.now()
q1_case1
## Sudoku(
## 5 4 9 | 6 2 8 | 3 7 1
## 8 3 2 | 7 1 4 | 5 9 6
## 7 6 1 | 9 3 5 | 4 2 8
## -------------------+-------------------+-------------------
## 2 9 8 | 3 7 1 | 6 5 4
## 6 1 3 | 4 5 2 | 9 8 7
## 4 7 5 | 8 6 9 | 2 1 3
## -------------------+-------------------+-------------------
## 1 5 4 | 2 8 3 | 7 6 9
## 9 8 6 | 5 4 7 | 1 3 2
## 3 2 7 | 1 9 6 | 8 4 5
## n: 3
## elements: 1, 2, 3, 4, 5, 6, 7, 8, 9
## empty: .
## )
## '0:00:00.070069'
q4_case1
, however, is a harder puzzle. You can see that .solve_globally()
doesn’t do anything:
## Sudoku(
## 3 . 2 | . . . | . 6 .
## . . . | 7 . 8 | 1 . .
## . . . | . . . | . . .
## -------------------+-------------------+-------------------
## 5 8 . | 4 . . | . . .
## . . . | . . . | . 1 2
## . . . | 1 . . | . . .
## -------------------+-------------------+-------------------
## . 1 . | . . . | 4 . .
## 6 . . | . 3 . | . . .
## . . . | . 2 . | . . .
## n: 3
## elements: 1, 2, 3, 4, 5, 6, 7, 8, 9
## empty: .
## )
## True
.solve_locally()
This method has by
argument, the name of a local group that it will work with. It must be specified, and must be one of either row
, col
, or submatrix
. It returns None
.
This looks at candidate values more carefully. It doesn’t look at the only candidate value remaining; rather, it tries to acquire the only existing candidate value within the local group it belongs, i.e. row
, col
, or submatrix
. If there is only one number that is exclusively a candidate value in one particular entry within a local group, that entry must have that value in its place. Just like .solve_globally()
, .solve_locally()
runs until no further mutation is made in Sudoku
puzzle.
Here’s an example:
## 1.3..2.........4
q_small
is a 4-by-4 (or 22-by-22) sudoku puzzle:
## Sudoku(
## 1 . | 3 .
## . 2 | . .
## --------------+--------------
## . . | . .
## . . | . 4
## n: 2
## elements: 1, 2, 3, 4
## empty: .
## )
Take a look at the 2nd row and candidate values in each entry of it:
## Candidate(
## {
## (1, 0): {'3', '4'},
## (1, 2): {'1', '4'},
## (1, 3): {'1'},
## },
## elements = {'3', '1', '4', '2'}
## )
## (n: 2)
The entry at the 2nd row and:
- the 1st column (i.e. entry
(1, 0)
) has3
and4
as candidate values because:2
is in the same row1
is in the same column1
and2
are in the same submatrix
- the 3rd column (i.e. entry
(1, 2)
) has1
and4
as candidate values because:2
is in the same row3
is in the same column and submatrix
- the 4th column (i.e. entry
(1, 3)
) has1
as the only candidate value because:2
is in the same row4
is in the same column3
is in the same submatrix
Although the entry at the 2nd row and the 1st column has two candidate values (3
and 4
), we can conclude that 3
must be the value at this entry because 3
is the only candidate value in the 2nd row that exists at this entry only.
puzzle
in step 4 above is an example of a puzzle that needs .solve_locally()
to get to the answer form:
## Sudoku(
## . . 3 | . . . | . 2 .
## . . . | 4 1 6 | . . 5
## 6 . 8 | . . . | . . .
## -------------------+-------------------+-------------------
## . 2 . | 9 . 4 | . 6 .
## . 6 . | . 8 . | . 7 .
## . 3 . | 2 . 7 | . 9 .
## -------------------+-------------------+-------------------
## . . . | . . . | 6 . 4
## 2 . . | 8 4 1 | . . .
## . 5 . | . . . | 9 . .
## n: 3
## elements: 1, 2, 3, 4, 5, 6, 7, 8, 9
## empty: .
## )
## Sudoku(
## 5 4 3 | 7 9 8 | 1 2 6
## 9 7 2 | 4 1 6 | 8 3 5
## 6 1 8 | 3 5 2 | 7 4 9
## -------------------+-------------------+-------------------
## 7 2 1 | 9 3 4 | 5 6 8
## 4 6 9 | 1 8 5 | 2 7 3
## 8 3 5 | 2 6 7 | 4 9 1
## -------------------+-------------------+-------------------
## 3 8 7 | 5 2 9 | 6 1 4
## 2 9 6 | 8 4 1 | 3 5 7
## 1 5 4 | 6 7 3 | 9 8 2
## n: 3
## elements: 1, 2, 3, 4, 5, 6, 7, 8, 9
## empty: .
## )
## Sudoku(
## . . 3 | . . . | . 2 .
## . . . | 4 1 6 | . . 5
## 6 . 8 | . . . | . . .
## -------------------+-------------------+-------------------
## . 2 . | 9 . 4 | . 6 .
## . 6 . | . 8 . | . 7 .
## . 3 . | 2 . 7 | . 9 .
## -------------------+-------------------+-------------------
## . . . | . . . | 6 . 4
## 2 . . | 8 4 1 | . . .
## . 5 . | . . . | 9 . .
## n: 3
## elements: 1, 2, 3, 4, 5, 6, 7, 8, 9
## empty: .
## )
## Sudoku(
## 5 4 3 | 7 9 8 | 1 2 6
## 9 7 2 | 4 1 6 | 8 3 5
## 6 1 8 | 3 5 2 | 7 4 9
## -------------------+-------------------+-------------------
## 7 2 1 | 9 3 4 | 5 6 8
## 4 6 9 | 1 8 5 | 2 7 3
## 8 3 5 | 2 6 7 | 4 9 1
## -------------------+-------------------+-------------------
## 3 8 7 | 5 2 9 | 6 1 4
## 2 9 6 | 8 4 1 | 3 5 7
## 1 5 4 | 6 7 3 | 9 8 2
## n: 3
## elements: 1, 2, 3, 4, 5, 6, 7, 8, 9
## empty: .
## )
.solve_by_pointing_pairs()
(As of version 0.1.0, this only works with by = 'submatrix'
. Working methods for by = 'row'
and by = 'col'
will be supported in version 0.1.1)
This method takes:
by
argument,submatrix
by default, and;start
argument,None
by default, which is an initialCandidate
that the method will start to reduce.
It returns a Candidate
class reduced down to the furthest level achievable by the method itself. If start
is specified, then this method will not only mutate Sudoku
, but also mutate start
AND return the reduced version of start
.
Inside a submatrix, if a single row or column exclusively contains one candidate number, then that candidate number should be eliminated from the same row or column that is not a part of the submatrix.
Here is an example:
pzl = '4873...9....6..271126.9.3847.5...162...2..8..........9..1.769233..1..45.....53.18'
pzl = ss.to_sudoku(pzl, elements = set([i for i in range(1, 10)]))
pzl
## Sudoku(
## 4 8 7 | 3 . . | . 9 .
## . . . | 6 . . | 2 7 1
## 1 2 6 | . 9 . | 3 8 4
## -------------------+-------------------+-------------------
## 7 . 5 | . . . | 1 6 2
## . . . | 2 . . | 8 . .
## . . . | . . . | . . 9
## -------------------+-------------------+-------------------
## . . 1 | . 7 6 | 9 2 3
## 3 . . | 1 . . | 4 5 .
## . . . | . 5 3 | . 1 8
## n: 3
## elements: 1, 2, 3, 4, 5, 6, 7, 8, 9
## empty: .
## )
See the candidate values in the fourth submatrix:
## Candidate(
## {
## (3, 1): {'3', '9', '4'},
## (4, 0): {'6', '9'},
## (4, 1): {'3', '6', '4', '9', '1'},
## (4, 2): {'3', '9', '4'},
## (5, 0): {'6', '8', '2'},
## (5, 1): {'3', '1', '6', '4'},
## (5, 2): {'3', '8', '2', '4'},
## },
## elements = {'3', '6', '4', '7', '9', '2', '1', '8', '5'}
## )
## (n: 3)
See the entries with keys (5, x)
(i.e. entries of the 6th row in the sudoku puzzle that belong to the 4th submatrix). Notice that 8
exclusively exists in the candidate value sets of entries (5, x)
within this submatrix. This means any entry that:
- belongs to the 6th row, and
- does NOT belong to the 4th submatrix, and
- has
8
as one of its candidate values
should eliminate 8
from one of its candidate values. That is, among those entries of the 6th row:
## Candidate(
## {
## (5, 0): {'6', '8', '2'},
## (5, 1): {'3', '1', '6', '4'},
## (5, 2): {'3', '8', '2', '4'},
## (5, 3): {'7', '8', '5', '4'},
## (5, 4): {'3', '6', '4', '1', '8'},
## (5, 5): {'4', '7', '1', '8', '5'},
## (5, 6): {'7', '5'},
## (5, 7): {'3', '4'},
## },
## elements = {'3', '6', '4', '7', '9', '2', '1', '8', '5'}
## )
## (n: 3)
the entries (5, 3)
, (5, 4)
, and (5, 5)
should get rid of 8
from one of its candidate values since they do not belong to the 4th submatrix and has 8
as one of its candidate values.
.solve_logically()
This method takes no argument. It returns either None
or Candidate
, depending on the circumstances:
- It returns
None
ifSudoku
is successfully solved. - It returns
Candidate
if logical approaches in this method are not sufficient to solve a puzzle.
This method applies:
.solve_globally()
.solve_locally(by)
, whereby = 'row'
,'col'
, and'submatrix'
.solve_by_pointing_pairs()
.solve_by_hidden_pairs(by)
, whereby = 'row'
,'col'
, and'submatrix'
It continues to apply these methods until either no mutation is made to Sudoku
, or Sudoku
transforms into the answer form.
.solve_forcefully()
This method takes:
max_trial
,200
by default, which is the maximum number of attempts to be triedquietly
,False
by default, which prints the message while solving forcefullyseed
(None
by default) which is a natural number to put intorandom.seed()
. IfNone
, thenrandom.seed()
is not set.
It returns int
, the number of attempts tried.
In this method, the following steps are repeated:
- Apply
.solve_logically()
and get the most refined version ofCandidate
fromSudoku
. - From the first entry (i.e. the entry with the smallest
row
andcol
in(row, col)
) ofSudoku
that isempty
, randomly plug in one value from the candidate set. - Apply
.solve_logically()
after plugging in. - Repeat 2 and 3 until:
- 4.1.
Sudoku
transforms into the answer form, OR - 4.2. at least one candidate set of
Sudoku
entries isset()
, i.e. the empty set.
- 4.1.
In case of 4.2., Sudoku
transforms back to the initial state, the method adds 1
to the trial
number and goes back to step 1. This process continues until either trial == max_trial
(200
by default) or Sudoku
transforms into the answer form.
.solve()
This method has max_trial
, quietly
, and seed
arguments, which are the same arguments as Sudoku.solve_forcefully()
. It returns a tuple of (str, int)
, where the first item is the string representation of datetime.timedelta
(e.g. '0:00:03.140492'
), and the second item is the return value of Sudoku.solve_forcefully()
. That is, the second item will be 0
if a puzzle is solved by Sudoku.solve_logically()
only.
This method first uses .solve_logically()
, and then .solve_forcefully()
.
Performance
See analysis for details.