Abstract: In this Java Specialists' Newsletter, we look at a simple Java program that solves SuDoKu puzzles.
Welcome to the 128th edition of The Java(tm) Specialists' Newsletter. On my flight from Cape Town to Frankfurt en route to TSSJS in Barcelona, I was reading about an attempted hijacking aboard a South African Airways (SAA) flight last Saturday. The flight attendants all seemed a bit skittish, not their usual friendly selves. But that was not what caught my attention. I saw a copy of a SuDoKu puzzle in the newspaper I was reading. You would have seen them around, together with a claim that this was the latest craze. I don't enjoy puzzles and games that can be handled by brute force number crunching, so up to now I had never attempted one.
So whilst flying at 30'000 feet, I wrote a little algorithm that solves them. There will be much better and faster approaches than mine, but it solved all the puzzles I threw at it in milliseconds, and I don't need to be much faster than that :)
javaspecialists.teachable.com: Please visit our new self-study course catalog to see how you can upskill your Java knowledge.
The program was written at 30'000 with a severe shortage of oxygen, so please excuse that it is not the most object-oriented in the world. It is also more complex than need be.
We start with the SuDoKu class, which we initialise with 81 integers. These must all be between 0 and 9, where 0 means that the cell is empty. We then call the solve() method and sit back and wait until the answer pops out a millisecond later.
Some notes about the code. Each cell on the gameboard holds an EnumSet of all possible values for that cell. When we reach an EnumSet of size 1, we have found a solution for that cell. If we end up with size 0, then there is no solution for the game.
I alternatively go through sieving the numbers that are on the board and searching for possible answers. I keep on going until I cannot reduce any more numbers, at which point there will be several answers to the puzzle. We could expand the program by then making a nondeterministic choice and continuing.
package com.cretesoft.sudoku; import java.util.*; public class SuDoKu { public enum Value { ONE, TWO, THREE, FOUR, FIVE, SIX, SEVEN, EIGHT, NINE; public String toString() { return Integer.toString(ordinal() + 1); } } private static final int GRID = 9; private final Map<Position, EnumSet<Value>> numbers = new HashMap<Position, EnumSet<Value>>(); public SuDoKu(int... values) { if (values.length != GRID * GRID) throw new IllegalArgumentException("Bad value count"); Value[] all = Value.values(); for (int i = 0; i < values.length; i++) { Position pos = new Position(i / GRID, i % GRID); if (values[i] == 0) { numbers.put(pos, EnumSet.range(Value.ONE, Value.NINE)); } else { numbers.put(pos, EnumSet.of(all[values[i] - 1])); } } sieveImpossibleNumbers(); } public boolean solve() { do { System.out.println(this); } while (sieveImpossibleNumbers() || searchForAnswers()) ; for (EnumSet<Value> values : numbers.values()) { if (values.size() != 1) return false; } return true; } /** * Goes through all the positions and removes numbers that are * not possible. Also checks the correctness of the found * numbers. */ private boolean sieveImpossibleNumbers() { boolean removed = false; for (Position pos : numbers.keySet()) { Value value = getNumber(pos); if (value == null) { // must be bitwise OR, otherwise it will fall through removed |= removeImpossibleNumbers(pos); } else { checkCorrectness(pos, value); } } return removed; } private boolean removeImpossibleNumbers(Position pos) { boolean removed = false; EnumSet<Value> vals = numbers.get(pos); for (Position other : pos.getRelatedPositions()) { removed |= vals.remove(getNumber(other)); } return removed; } private Value getNumber(Position pos) { EnumSet<Value> vals = numbers.get(pos); if (vals.size() == 1) { return vals.iterator().next(); } return null; } private void checkCorrectness(Position pos, Value val) { for (Position other : pos.getRelatedPositions()) { if (val == getNumber(other)) { throw new IllegalArgumentException("Error with: " + pos + " clashes with relative " + other); } } } private boolean searchForAnswers() { for (Position pos : numbers.keySet()) { EnumSet<Value> possible = numbers.get(pos); if (possible.size() > 1) { for (Value value : possible) { if (valueNotIn(value, pos.getHorizontalPositions()) || valueNotIn(value, pos.getVerticalPositions()) || valueNotIn(value, pos.getSmallSquarePositions())) { System.out.println(pos + " MUST BE " + value); numbers.put(pos, EnumSet.of(value)); return true; } } } } return false; } private boolean valueNotIn(Value value, Collection<Position> positions) { for (Position pos : positions) { if (numbers.get(pos).contains(value)) { return false; } } return true; } public String toString() { StringBuffer result = new StringBuffer(); for (int row = 0; row < GRID; row++) { for (int col = 0; col < GRID; col++) { EnumSet<Value> vals = numbers.get(new Position(row, col)); result.append('['); for (Value v : vals) result.append(v); result.append(']').append('\t'); } result.append('\n'); } return result.toString(); } }
The Position class represents a place on the gameboard. It also can tell me which other positions are related to it, either horizontally, vertically or in a small 3x3 box.
package com.cretesoft.sudoku; import java.util.*; public final class Position { private final int row; private final int col; public Position(int row, int col) { this.row = row; this.col = col; } public int hashCode() { return row * 29 + col; } public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Position)) return false; Position position = (Position) o; return !(col != position.col || row != position.row); } public String toString() { return "(" + row + "," + col + ")"; } public Collection<Position> getRelatedPositions() { Collection<Position> result = new HashSet<Position>(); result.addAll(getHorizontalPositions()); result.addAll(getVerticalPositions()); result.addAll(getSmallSquarePositions()); return result; } public Collection<Position> getHorizontalPositions() { Collection<Position> result = new HashSet<Position>(); for (int i = 0; i < 9; i++) { result.add(new Position(row, i)); } result.remove(this); return result; } public Collection<Position> getVerticalPositions() { Collection<Position> result = new HashSet<Position>(); for (int i = 0; i < 9; i++) { result.add(new Position(i, col)); } result.remove(this); return result; } public Collection<Position> getSmallSquarePositions() { Collection<Position> result = new HashSet<Position>(); for (int i = 0; i < 9; i++) { int smallSqRow = i / 3 + (row / 3) * 3; int smallSqCol = i % 3 + (col / 3) * 3; result.add(new Position(smallSqRow, smallSqCol)); } result.remove(this); return result; } }
We can try it out like this:
package com.cretesoft.sudoku; public class SuDoKuTest { public static void main(String[] args) { SuDoKu gb = new SuDoKu( // Cape Times Mon 2006/06/19 2, 0, 0, 9, 0, 6, 0, 0, 4, 0, 0, 5, 0, 7, 0, 9, 0, 0, 0, 3, 0, 0, 0, 0, 0, 8, 0, 0, 0, 3, 4, 0, 7, 8, 0, 0, 8, 9, 0, 2, 0, 5, 0, 6, 3, 0, 0, 7, 6, 0, 8, 2, 0, 0, 0, 7, 0, 0, 0, 0, 0, 2, 0, 0, 0, 8, 0, 6, 0, 1, 0, 0, 3, 0, 0, 7, 0, 1, 0, 0, 8); if (gb.solve()) { System.out.println("SOLVED!!!"); } else { System.out.println("Could not solve"); } } }
When we run this SuDoKuTest, we see the various steps taken in order to solve it. So you could also use this as a tutor to show you how the answer is derived, if you are into SuDoKu.
2 8 1 9 5 6 3 7 4 4 6 5 8 7 3 9 1 2 7 3 9 1 2 4 5 8 6 6 2 3 4 9 7 8 5 1 8 9 4 2 1 5 7 6 3 5 1 7 6 3 8 2 4 9 1 7 6 3 8 9 4 2 5 9 4 8 5 6 2 1 3 7 3 5 2 7 4 1 6 9 8
Kind regards
Heinz
We are always happy to receive comments from our readers. Feel free to send me a comment via email or discuss the newsletter in our JavaSpecialists Slack Channel (Get an invite here)
We deliver relevant courses, by top Java developers to produce more resourceful and efficient programmers within their organisations.