The next rules to implement for the Sudoku solver are called wing rules. There are a few of these rules used in solving Sudoku, but some are hard to identify and are for very advanced solves. The two needed to solve most Sudoku puzzles are called XY-Wing and XYZ-Wing:
- XY-Wing uses a pivot cell with two candidates
XYand two wing cells, one with theXZcandidates and the other theYZcandidates. - XYZ-Wing is similar, but the pivot cell has all three candidates,
XYZ, in it.
If you like this post and you’d like to know more about how to plan and write Python software, check out the Python tag.
What are the Wing Rules?
The wing rules all involve a pivot cell. This cell is surrounded by two wing cells which share a house with the pivot cell. For a better explanation than I’m going to give, check out the XY-Wing page and the XYZ-Wing page from the Sudopedia site.
Let’s look at each individually before we look at the solving code!
XY-Wing
These come in two varieties, either a box-line version or a row-column version. They both apply with the same pattern though.
The pivot cell must contain exactly two candidates, X and Y.
The wing cells must share a house with the pivot cell and one must contain candidates X and Z, while the other contains candidates Y and Z.
Z is the value which can be removed from all other cells that share houses with both wing cells.
Let’s look at this pattern with a type 1 XY-Wing where one wing shares a row or column with the pivot cell and the other shares a box.
.-----------.-----------.-----------.
| * P * | . . W | . . . |
| . . . | . . . | . . . |
| W . . | * * * | . . . |
'-----------'-----------'-----------'
P is the pivot cell and might contain, for example, candidates 1 and 5 (X and Y) only.
The W in the same row might contain only 1 and 9 (X and Z) as candidates.
The W in the same box would have to contain candidates 5 and 9 (Y and Z) to complete the XY-Wing pattern.
The cells that are visible to both wings are marked with * and candidates Z (9) can be removed from all of these.
The other form of this pattern is row-column and looks like the following.
.-----------.-----------.-----------.
| . . . | . . . | . . . |
| . P . | . . W | . . . |
| . . . | . . . | . . . |
----------- ----------- -----------
| . . . | . . . | . . . |
| . . . | . . . | . . . |
| . W . | . . * | . . . |
'-----------'-----------'-----------'
In this case, there are no wings sharing a box with pivot cell. They still contain the same candidates as before though:
Phas only candidatesXandY- one
Wcell has only candidatesXandZ - the other
Wcell has only candidatesYandZ
In this case, there is only one cell which can be seen by both wings.
The Z candidate can be removed from this cell.
XYZ-Wing
The only difference in this pattern is that the pivot cell contains all the candidates X, Y and Z and one of the wings must share a box with the pivot cell.
The cells which can remove the Z candidate must be able to see both wings and the pivot cell in this pattern.
Let’s look at an example.
.-----------.-----------.-----------.
| * P * | . . W | . . . |
| . . . | . . . | . . . |
| W . . | . . . | . . . |
'-----------'-----------'-----------'
- The pivot cell,
P, must contain only candidatesX,YandZ. - One wing must contain only
XandZcandidates. - The other wing must contain only
YandZcandidates.
It looks very similar to the XY-Wing pattern above in a box-line arrangement.
However, the affected cells in the middle box are no longer valid to remove the Z candidate from.
Implementing the Rules
The current code at the time of writing can be found on GitHub where you can also see the pull request for the changes in this post.
I also tidied up the tests for the solver.py file in this pull request as they were getting unmanageable.
Finding XY-Wings
applied = False
# Find an XY-Wing pattern.
for pivot in [cell for cell in grid if len(cell.candidates) == 2]:
xy = pivot.candidates
# Wings must have 2 candidates and share only one with the pivot.
wings = [
wing
for wing in grid.get_neighbours(pivot)
if len(wing.candidates) == 2 and len(xy.difference(wing.candidates)) == 1
]
for wing1, wing2 in combinations(wings, 2):
# Reject the wings if they don't share a symmetric difference with the pivot.
if wing1.candidates.symmetric_difference(wing2.candidates) != xy:
continue
# Find a z-value common to both wings.
z_set = wing1.candidates.intersection(wing2.candidates)
if len(z_set) != 1:
continue
z = z_set.pop()
# Eliminate z from cells that see both wings of the XY-Wing.
wing1_neighbours = grid.get_neighbours(wing1)
wing2_neighbours = grid.get_neighbours(wing2)
common_neighbours = wing1_neighbours.intersection(wing2_neighbours)
for cell in [
cell
for cell in common_neighbours
if z in cell.candidates and cell != pivot
]:
cell.candidates -= {z}
applied = True
return applied
Let’s go over how this works:
- Line 1 sets us up with a variable to return if we make any changes to the grid.
- Line 4 iterates over potential pivots from all cells in the grid that have exactly 2 candidates.
- Line 5 defines a set containing the
XYcandidates. - Lines 8 to 12 establishes a list of possible wings, where they must be neighbours of the pivot cell, must have exactly 2 candidates, and only contain one of the
XYcandidates. - Line 14 iterates over pairs of potential wings.
- In lines 16 and 17, if the two wings do not contain
XandYcandidates between them, they are skipped as they don’t fit the pattern. - Lines 20 to 23 reject a pair of wings if they don’t also contain the same
Zcandidate. - Lines 26 to 28 are only reached if the pattern matches, and they work out which cells could have the
Zcandidate removed from them. - Lines 30 to 36 remove the
Zcandidate from the identified cells, if they contain it. If this is possible, theappliedvariable is changed toTrueas the grid was changed.
Finding XYZ-Wings
applied = False
# Find an XYZ-Wing pattern.
for pivot in [cell for cell in grid if len(cell.candidates) == 3]:
xyz = pivot.candidates
# Wings must have 2 candidates that are a subset of the pivot's candidates.
wings = [
wing
for wing in grid.get_neighbours(pivot)
if len(wing.candidates) == 2 and wing.candidates.issubset(xyz)
]
for wing1, wing2 in combinations(wings, 2):
# Find the z-value common to both wings.
z_set = wing1.candidates.intersection(wing2.candidates)
if len(z_set) != 1:
continue
z = z_set.pop()
# Eliminate z from cells that see all three of the XYZ-Wing cells.
pivot_neighbours = grid.get_neighbours(pivot)
wing1_neighbours = grid.get_neighbours(wing1)
wing2_neighbours = grid.get_neighbours(wing2)
common_neighbours = pivot_neighbours.intersection(
wing1_neighbours
).intersection(wing2_neighbours)
for cell in [cell for cell in common_neighbours if z in cell.candidates]:
cell.candidates -= {z}
applied = True
return applied
This logic is almost identical to the logic for an XY-Wing with the following differences:
- Line 4 finds pivot cells as those with exactly 3 candidates.
- Line 11 identifies wing cells as those who have 2 candidates, both of which have to be shared with the pivot.
- Line 22 to 27 find the cells that can have the
Zcandidate removed by finding those which share a house with the pivot and both wing cells.
Testing Our Code
The tests are very similar to other rules we’ve created.
In each case, I check that we can find the wings we expect to find, that we get a True result when a wing was found, that we get a False result when there is no wing, and that we get a False result when the grid is complete.
Solving Puzzles
Running the new code against our existing “expert” puzzles shows that we can now solve the one labelled expert_02 so we’ve made progress!
The other two expert puzzles still do not fully solve, but they are more solved than they were before these rules.
We still need to implement more rules.
Wrapping Up
We’ve implemented and applied the XY-Wing and the XYZ-Wing rules for our Sudoku solver. I’ve checked what’s blocking us from solving the other expert puzzles and it seems like the next step to solve these requires the Nishio approach. We’ll have a go at implementing this next time we work on the solver. I’m a little disappointed that this technique is needed because it’s less about logic, and more about trial and error.
We’ll take a look at an approach to that next time.
