(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:

import sudsoln as ss
print(ss.__version__)
## 0.1.0

Initializing a puzzle

There are two ways to initialize a puzzle:

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:

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:

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 in array/sudoku_str that is NOT empty

Notes:

  1. Unlike empty, elements has no default argument.
  2. Usually, if a size of puzzle is 9-by-9 (or 32-by-32), then elements is a set of integers from 1 to 9.
  3. Every element in elements as well as empty must have length 1 if it is converted into type str. For example, 10 or 14 cannot be an element of elements, or '..' cannot be empty.

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:

.solve_globally()

This method accepts no argument, and returns None.

For each entry, this method looks at the row, column, 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, and 9 in the row it belongs
  • there are 1 and 2 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:

## 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)) has 3 and 4 as candidate values because:
    • 2 is in the same row
    • 1 is in the same column
    • 1 and 2 are in the same submatrix
  • the 3rd column (i.e. entry (1, 2)) has 1 and 4 as candidate values because:
    • 2 is in the same row
    • 3 is in the same column and submatrix
  • the 4th column (i.e. entry (1, 3)) has 1 as the only candidate value because:
    • 2 is in the same row
    • 4 is in the same column
    • 3 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 initial Candidate 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:

## 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:

  1. belongs to the 6th row, and
  2. does NOT belong to the 4th submatrix, and
  3. 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_by_hidden_pairs()

This method takes:

  • by argument, submatrix by default, and;
  • start argument, None by default, which is an initial Candidate that the method will start to reduce.

This method works with all possible bys, which are row, col, and submatrix. And 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.

Within a local group by (== row, col, or submatrix), if two distinct entries have two candidate values exclusively, then these two entries should only contain those two values in its set of candidate values.

For example, consider the following Candidate group:

See (5, 0) and (5, 2). These two entries contain 2 and 8 exclusively in this group, so the set of candidate values should be {'2', '8'} for both.

.solve_logically()

This method takes no argument. It returns either None or Candidate, depending on the circumstances:

  • It returns None if Sudoku 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), where by = 'row', 'col', and 'submatrix'
  • .solve_by_pointing_pairs()
  • .solve_by_hidden_pairs(by), where by = '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 tried
  • quietly, False by default, which prints the message while solving forcefully
  • seed (None by default) which is a natural number to put into random.seed(). If None, then random.seed() is not set.

It returns int, the number of attempts tried.

In this method, the following steps are repeated:

  1. Apply .solve_logically() and get the most refined version of Candidate from Sudoku.
  2. From the first entry (i.e. the entry with the smallest row and col in (row, col)) of Sudoku that is empty, randomly plug in one value from the candidate set.
  3. Apply .solve_logically() after plugging in.
  4. Repeat 2 and 3 until:
    • 4.1. Sudoku transforms into the answer form, OR
    • 4.2. at least one candidate set of Sudoku entries is set(), i.e. the empty set.

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.