Home of The JavaSpecialists' Newsletter

128SuDoKu Madness

Posted: 2006-06-21Category: Tips and TricksJava Version: 1.5+Dr. Heinz M. Kabutz
 

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

NEW: Refactoring to Java 8 Lambdas and Streams Workshop Are you currently using Java 6 or 7 and would like to see how Java 8 can improve your code base? Are you tired of courses that teach you a whole bunch of techniques that you cannot apply in your world? Check out our one day intensive Refactoring to Java 8 Lambdas and Streams Workshop.

SuDoKu Madness

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

 

Related Articles

Browse the Newsletter Archive

About the Author

demo

Java Champion, author of the Javaspecialists Newsletter, conference speaking regular... About Heinz

Java Training

We deliver relevant courses, by top Java developers to produce more resourceful and efficient programmers within their organisations.

Java Consulting

Nobody ever wants to call a Java performance consultant, but with first-hand experience repairing and improving commercial Java applications - JavaSpecialists are a good place to start...

Threading Emergency?

If your system is down, we will review it for 15 minutes and give you our findings for just 1 € without any obligation.