Sudoku is a fairly well-known type of puzzle. Solving it turns out to be easier than I expected, but a somewhat odd example of test-driven development. |
There’s been discussion on the extremeprogramming group about solving Sudoku puzzles. I saw Ron Jeffries first do a spike, and then start solving. Then Cory Foy pitched in his version as well. I skipped to the bottom of his article and found out it wasn’t doing an exhaustive search, but rather looking for simple “forced” cells.
From the discussion, I had decided I wouldn’t be fancy on the indexing: I’ll just use a simple array, and define constraints as indexes on the array. I also decided to first do an exhaustive depth-first search, and then performance-tune if necessary.
First Test, Unusually Large
Here’s my first test:
public void testBoardThatHasToApplyConstraints() { Board result = new Solver().solve(board, constraints); |
This isn’t a real Sudoku puzzle but it’s close enough to get me started.
I don’t often start with a test this big but I wanted to capture the direction I plan to go. I used this chance to create stubs for the Board, Constraint, and Solver classes and their methods:
public class Board { |
As you’d expect, this test comes back red. It’s definitely too advanced for where we are. So I’m going to rename the failing test method to xtestBoardThatHasToApplyConstraints()
, (so it won’t run) and take a smaller bite, what Kent Beck calls a Child Test.
Board Equality
My smaller step will be to test whether two boards are equal. It’s much easier, and I have to do it anyways to make the final “assertEquals()
” work, so it’s a good step. I implemented the next series of tests one at a time, but I’ll show them all together:
public class BoardTest extends TestCase { |
The implementation is what you’d expect:
public class Board { |
A Small, Full Grid
Rather than tackle that large test I started with, I have a much simpler case. Let’s look at a board with everything filled in, under no constraints.
public void |
A straightforward solution makes that work:
public Board solve(Board board, ArrayList constraints) { if (solved(board, constraints)) return board; throw new Error("No solution found"); } private boolean solved( Board board, ArrayList constraints) { return true; } |
This next test is the same, but with constraints added. Since we always return the board we were given, we expect it to pass (and it does). A Constraint is a set of “n” positions; the constraint it enforces is that each number 1..n must appear exactly once in the cells with those positions.
public void testSolvedBoardComesBackAsIsWithConstraints() { Board board = new Board(new int[] { 1, 2, 2, 1 }); ArrayList constraints = new ArrayList(); constraints.add(new Constraint(new int[] { 0, 1 })); constraints.add(new Constraint(new int[] { 2, 3 })); constraints.add(new Constraint(new int[] { 0, 2 })); constraints.add(new Constraint(new int[] { 1, 3 })); Board result = new Solver().solve(board, constraints); assertEquals(board, result); } |
I’ll do a bit of “Fake It” here. I want to say the board is solved if no constraint says it isn’t. I’ll run through the constraints, but for now they’ll all answer “true” (i.e., that the puzzle is solved as far as that constraint is concerned). The tests stay green
// In Board.java: private boolean solved( Board board, ArrayList constraints) { for (int i = 0; i < constraints.size(); i++) { Constraint constraint = (Constraint) constraints.get(i); // In Constraint.java: public boolean solvedBy(Board board) { return true; } |
To show that Constraint isn’t picky enough, let’s add a test:
public void |
Constraint makes a list of values used and returns false if any (digits 1 or greater) is not exactly 1.
// In Constraint.java: public boolean solvedBy(Board board) { int[] valuesUsed = new int[board.width()+1]; for (int i = 0; i < indexes.length; i++) valuesUsed[board.at(indexes[i])]++; for (int i = 1; i < valuesUsed.length; i++) if (valuesUsed[i] != 1) return false; return true; } // In Board.java: public class Board { int[] cells; int width; public Board(int[] cells) { this.cells = cells; width = (int) Math.sqrt(cells.length); } public int width() { return width; } public int at(int i) { return cells[i]; } // etc. } |
Validity
We have the notion of whether a puzzle is solved (all constraints say they are solved). But there’s another notion we need along the way: whether a board configuration is valid. A board is valid if it has no constraint that says it’s not. And the constraints will allow 0s (empty cells), but not two cells that claim to own the same number. So: empty cells in a row is valid, but two 7s in a row are not.
public void testBoardIsValidIfNoConstraintsAreContradicted() { Board board = new Board(new int[] { 1, 2, 0, 1 }); ArrayList constraints = new ArrayList(); constraints.add(new Constraint(new int[] { 2, 3 })); assertTrue(new Solver().valid(board, constraints)); } |
We can make valid() return true and get to green. It needs to do more when we test the other way:
public void testBoardIsInvalidIfAnyConstraintIsContradicted() { Board board = new Board(new int[] { 1, 2, 1, 1 }); ArrayList constraints = new ArrayList(); constraints.add(new Constraint(new int[] { 2, 3 })); assertFalse(new Solver().valid(board, constraints)); } |
// In Solver.java: public boolean valid(Board board, ArrayList constraints) { for (int i = 0; i < constraints.size(); i++) { Constraint constraint = (Constraint) constraints.get(i); if (!constraint.accepts(board)) return false; } return true; } // In Constraint.java: public boolean accepts(Board board) { int[] valuesUsed = new int[board.width()+1]; for (int i = 0; i < indexes.length; i++) valuesUsed[board.at(indexes[i])]++; for (int i = 1; i < valuesUsed.length; i++) if (valuesUsed[i] > 1) return false; return true; } |
If you look back at the earlier Constraint.solvedBy()
method, you’ll see that the first half of these two methods is identical. We’ll extract a helper method:
public boolean solvedBy(Board board) { int[] valuesUsed = valuesUsed(board); for (int i = 1; i < valuesUsed.length; i++) if (valuesUsed[i] != 1) return false; return true; } public boolean accepts(Board board) { int[] valuesUsed = valuesUsed(board); for (int i = 1; i < valuesUsed.length; i++) if (valuesUsed[i] > 1) return false; return true; } private int[] valuesUsed(Board board) { int[] valuesUsed = new int[board.width()+1]; for (int i = 0; i < indexes.length; i++) valuesUsed[board.at(indexes[i])]++; return valuesUsed; } |
There’s still duplication – those loops are very similar – but we’ll leave it. In Ruby or Smalltalk, it might be easier to get rid of.
Depth-First Search
We’re up to the edge of solving real Sudoku. When I do them by hand, I have a handful of strategies (look for forced cells, consider both down and across, etc.) But at least for my skill level, the hard problems have always required me to do a search: try it one way and see what happens, and backtrack if that fails.
My worst-case strategy is a standard strategy from AI: depth-first search. One algorithm for that says:
- Make a stack and push the initial candidates.
- While there are still things in the stack:
-
- Pop the top item.
- If it’s a solution, return it.
- If it’s not a solution but it’s valid, generate any consequent candidates and push them on the stack.
- If it’s neither a solution nor valid, don’t push anything
-
- If you have an empty stack at the end, you found no solution.
To represent my candidates, I’ll have a Candidate class. It’s just a data bag with three things we need to track: the board, the next index to change, and the value to put there. It’s one of those rare classes so simple I won’t write a test for it.
public class Candidate { private Board board; private int index; private int value; public Candidate(Board board, int index, int value){ this.board = board; this.index = index; this.value = value; } public Board board() { return board; } public int index() { return index; } public int value() { return value; } } |
In a search like this, we’ll frequently take a board, and want to modify one cell. If we work on a copy of the board, backtracking is easy – just don’t use that copy any more.
public void testBoardCanBeBuiltWithOneValueChanged() { Board board2 = new Board(board, 0, 2); assertEquals(2, board2.at(0)); assertEquals(new Board(new int[] { 2, 2, 0, 1 }), |
Create that new constructor, copying the cells and the width. (I forgot to copy the width on my first try:(
public Board(Board old, int index, int value) { this.cells = new int[old.cells.length]; for (int i = 0; i < cells.length; i++) cells[i] = old.cells[i]; width = old.width; cells[index] = value; } |
The next test can force the depth-first algorithm.
public void testBoardThatHasToApplyConstraints() { Board board = new Board(new int[] { 1, 2, 0, 1 }); ArrayList constraints = new ArrayList(); constraints.add(new Constraint(new int[] { 0, 1 })); constraints.add(new Constraint(new int[] { 2, 3 })); constraints.add(new Constraint(new int[] { 0, 2 })); constraints.add(new Constraint(new int[] { 1, 3 })); Board result = new Solver().solve(board, |
As I go through this exposition, this just seems like too big a step. Let’s try a smaller step that checks the generation of candidates.
public void testIfIndexIsTooBigThenNothingGetsPushed() { Board board = new Board(new int[] {1, 2, 0, 1}); Stack stack = new Stack(); new Solver().pushCandidates(stack, board, 4); assertTrue(stack.isEmpty()); } public void testIfCellAlreadyFilledOnlyThatBoardGetsPushed() { Board board = new Board(new int[] {1, 2, 0, 1}); Stack stack = new Stack(); new Solver().pushCandidates(stack, board, 0); assertEquals(1, stack.size()); assertEquals(new Candidate(board, 0, 1), |
This code makes those tests pass:
// In Solver.java: public void pushCandidates( Stack stack, Board board, int index) { if (index >= board.width() * board.width()) return; if (board.has(index)) { stack.push( new Candidate(board, index, board.at(index))); return; } for (int i = 1; i <= board.width(); i++) stack.push(new Candidate(board, index, i)); } // In Candidate.java: public boolean equals(Object obj) { if (obj == null || obj.getClass() != Candidate.class) return false; Candidate that = (Candidate) obj; return this.board.equals(that.board) && this.index == that.index && this.value == that.value; } // In Board.java: public boolean has(int index) { return cells[index] != 0; } |
Let’s return to testBoardThatHasToApplyConstraints()
. Now I’ll take a go at the search algorithm. Confession – I accidentally passed board
(rather than newBoard
) to pushCandidates()
, and used the debugger to see I had the wrong board.
public Board solve(Board board, ArrayList constraints) { Stack stack = new Stack(); pushCandidates(stack, board, 0); while (!stack.isEmpty()) { Candidate candidate = (Candidate) stack.pop(); Board newBoard = new Board( candidate.board(), candidate.index(), candidate.value()); if (solved(newBoard, constraints)) return newBoard; if (valid(newBoard, constraints)) pushCandidates( |
The next two tests pass already. One’s a small board, the other is the example from the Wikipedia article cited above. (I also tried a couple “hard” ones from the back of a Sudoku book, but I won’t include them here.)
public void test4x4Board() { Board board = new Board(new int[] { 1,2, 0,0, 0,0, 1,2, 0,1, 0,4, 4,0, 0,0 }); ArrayList constraints = new ArrayList(); // Row constraints constraints.add( new Constraint(new int[] { 0, 1, 2, 3 })); constraints.add( new Constraint(new int[] { 4, 5, 6, 7 })); constraints.add( new Constraint(new int[] { 8, 9, 10, 11 })); constraints.add( new Constraint(new int[] { 12, 13, 14, 15 })); // Column constraints constraints.add( new Constraint(new int[] { 0, 4, 8, 12 })); constraints.add( new Constraint(new int[] { 1, 5, 9, 13 })); constraints.add( new Constraint(new int[] { 2, 6, 10, 14 })); constraints.add( new Constraint(new int[] { 3, 7, 11, 15 })); // Box constraints constraints.add( new Constraint(new int[] { 0, 1, 4, 5 })); constraints.add( new Constraint(new int[] { 2, 3, 6, 7 })); constraints.add( new Constraint(new int[] { 8, 9, 12, 13 })); constraints.add( new Constraint(new int[] { 10, 11, 14, 15 })); Board result = new Solver().solve(board, constraints); Board expected = new Board(new int[] { 1,2, 4,3, 3,4, 1,2, 2,1, 3,4, 4,3, 2,1 }); assertEquals(expected, result); } public void testBigBoardWiki() { Board board = new Board(new int[] { 5,3,0, 0,7,0, 0,0,0, 6,0,0, 1,9,5, 0,0,0, 0,9,8, 0,0,0, 0,6,0, 8,0,0, 0,6,0, 0,0,3, 4,0,0, 8,0,3, 0,0,1, 7,0,0, 0,2,0, 0,0,6, 0,6,0, 0,0,0, 2,8,0, 0,0,0, 4,1,9, 0,0,5, 0,0,0, 0,8,0, 0,7,9 }); ArrayList constraints = constraints9x9(); Board result = new Solver().solve(board, |
Analysis
This snapped together fairly well. It’s taken me longer to write it up than it did to solve.
The oddest part for me was my first test. I wanted a test that was something like the real problem, and this was a good test for that. But it was a larger first step than I usually use. I suspect this is because I had a solution “trail” in mind.
There’s still room for some refactoring, though Java doesn’t make it particularly easy to get rid of similar loops. You could pull out a Constraints class (rather than the ArrayList); Solver.solved()
and Solver.valid()
could move over there. Or maybe Solver really should just be Constraints.
There’s some room for work in the Board class. I tried it with clone()
one time and with a constructor the other. A third alternative would be to put a copy-and-modify operation on Board (having the board generate a copy of itself with the cells modified). I’d keep that up my sleeve if I needed to later.
This had some of the flavor of re-implementing a known algorithm: I certainly had a certain approach in mind when I started. The steps didn’t feel so big when I did them the first time, but I felt a strong urge to fill in the blanks while writing it up. (I wonder which impulse is better.)
The algorithm itself turned out to be as nice as I expected. The setting up of constraints is somewhat ugly, but it was quick and easy. I made my list and checked it twice. If I were generalizing beyond this case, I’d definitely revisit constraint setup.
The biggest surprise for me was in performance. I had fully expected this program to need a lot of performance tuning. After all, I didn’t do anything to cut down the huge search space. But even the hardest boards I’ve tried run in about a second.
Resources
[Written July 10, 2006.]