package cx.prutser.sudoku.solver;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Solver for standard 9x9 sudoku's, based on a board layout with the following
* tile indexes:
* <PRE>
* 0 1 2 3 4 5 6 7 8
* +--+--+--+--+--+--+--+--+--+
* 0 | 0 1 2| 3 4 5| 6 7 8|
* 1 | 9 10 11|12 13 14|15 16 17|
* 2 |18 19 20|21 22 23|24 25 26|
* +--+--+--+--+--+--+--+--+--+
* 3 |27 28 29|30 31 32|33 34 35|
* 4 |36 37 38|39 40 41|42 43 44|
* 5 |45 46 47|48 49 50|51 52 53|
* +--+--+--+--+--+--+--+--+--+
* 6 |54 55 56|57 58 59|60 61 62|
* 7 |63 64 65|66 67 68|69 70 71|
* 8 |72 73 74|75 76 77|78 79 80|
* +--+--+--+--+--+--+--+--+--+
* </PRE>
*
* @author Erik van Zijst
*/
public class ClassicSolver implements Solver<Integer> {
private static final int HORIZONTAL_INDEX[] = new int[] {
0, 0, 0, 0, 0, 0, 0, 0, 0,
1, 1, 1, 1, 1, 1, 1, 1, 1,
2, 2, 2, 2, 2, 2, 2, 2, 2,
3, 3, 3, 3, 3, 3, 3, 3, 3,
4, 4, 4, 4, 4, 4, 4, 4, 4,
5, 5, 5, 5, 5, 5, 5, 5, 5,
6, 6, 6, 6, 6, 6, 6, 6, 6,
7, 7, 7, 7, 7, 7, 7, 7, 7,
8, 8, 8, 8, 8, 8, 8, 8, 8
};
private static final int VERTICAL_INDEX[] = new int[] {
0, 1, 2, 3, 4, 5, 6, 7, 8,
0, 1, 2, 3, 4, 5, 6, 7, 8,
0, 1, 2, 3, 4, 5, 6, 7, 8,
0, 1, 2, 3, 4, 5, 6, 7, 8,
0, 1, 2, 3, 4, 5, 6, 7, 8,
0, 1, 2, 3, 4, 5, 6, 7, 8,
0, 1, 2, 3, 4, 5, 6, 7, 8,
0, 1, 2, 3, 4, 5, 6, 7, 8,
0, 1, 2, 3, 4, 5, 6, 7, 8
};
private static final int AREA_INDEX[] = new int[] {
0, 0, 0, 1, 1, 1, 2, 2, 2,
0, 0, 0, 1, 1, 1, 2, 2, 2,
0, 0, 0, 1, 1, 1, 2, 2, 2,
3, 3, 3, 4, 4, 4, 5, 5, 5,
3, 3, 3, 4, 4, 4, 5, 5, 5,
3, 3, 3, 4, 4, 4, 5, 5, 5,
6, 6, 6, 7, 7, 7, 8, 8, 8,
6, 6, 6, 7, 7, 7, 8, 8, 8,
6, 6, 6, 7, 7, 7, 8, 8, 8
};
private Tile<Integer> tiles[];
private Constraint[][] constraintsByTile = new Constraint[81][];
private long evals = 0L;
private final long timeout;
private long start;
public ClassicSolver(Integer[] board) {
this(board, 0L); // no time constraint by default
}
public ClassicSolver(Integer[] board, long timeout) {
if (board == null || board.length != 81) {
throw new IllegalArgumentException("Board must contain 81 tiles.");
} else {
this.timeout = timeout;
tiles = new Tile[81];
for (int i = 0; i < board.length; i++) {
if (board[i] != null && (board[i] < 1 || board[i] > 9)) {
throw new IllegalArgumentException(
"Illegal value: " + board[i]);
} else {
tiles[i] = new Tile<Integer>(board[i]);
}
}
}
// create constraints:
final UniqueConstraint<Integer> horizontal[] = new UniqueConstraint[9];
final UniqueConstraint<Integer> vertical[] = new UniqueConstraint[9];
final UniqueConstraint<Integer> area[] = new UniqueConstraint[9];
for (int i = 0; i < 9; i++) {
horizontal[i] = new UniqueConstraint<Integer>();
vertical[i] = new UniqueConstraint<Integer>();
area[i] = new UniqueConstraint<Integer>();
}
for (int i = 0; i < 81; i++) {
final Set<Constraint> constraints = new HashSet<Constraint>();
if (tiles[i].getValue() != null) {
constraints.add(new FixedValueConstraint<Integer>(
tiles[i], tiles[i].getValue()));
}
// every tile is part of 3 unique collections
vertical[VERTICAL_INDEX[i]].addTile(tiles[i]);
horizontal[HORIZONTAL_INDEX[i]].addTile(tiles[i]);
area[AREA_INDEX[i]].addTile(tiles[i]);
constraints.add(vertical[VERTICAL_INDEX[i]]);
constraints.add(horizontal[HORIZONTAL_INDEX[i]]);
constraints.add(area[AREA_INDEX[i]]);
constraintsByTile[i] = constraints.toArray(
new Constraint[constraints.size()]);
}
}
public void solve(SolutionsCollector<Integer> integerSolutionsCollector) {
try {
start = System.currentTimeMillis();
solve(0, integerSolutionsCollector);
integerSolutionsCollector.searchComplete(evals);
} catch(CanceledException ce) {
}
}
/**
* Recursive call that implements the search algorithm. Every solution that
* is found is synchronously reported to the user through the specified
* {@link cx.prutser.sudoku.solver.SolutionsCollector} instance.
*
* @param index the index of the tile currently being evaluated.
* @param collector
* @throws CanceledException when the user canceled the search.
*/
private void solve(int index, SolutionsCollector<Integer> collector)
throws CanceledException {
if (index == tiles.length) {
reportSolution(collector);
} else if (isTimeUp()) {
/* IMPLEMENTATION NOTE:
*
* If there's overhead involved in checking this, we can limit the
* check to the lower ply's of the search tree.
*/
collector.timeoutExceeded(timeout);
throw new CanceledException();
} else {
if (tiles[index].getValue() != null) {
solve(index + 1, collector);
} else {
for (int i = 1; i <= 9; i++) {
tiles[index].setValue(i);
if(meetsConstraints(index)) {
solve(index + 1, collector);
}
}
tiles[index].setValue(null);
}
}
}
private boolean meetsConstraints(int tileNumber) {
for (Constraint constraint : constraintsByTile[tileNumber]) {
evals++;
if (!constraint.isSatisfied()) {
return false;
}
}
return true;
}
private boolean isTimeUp() {
return timeout > 0L && System.currentTimeMillis() - start > timeout;
}
/**
* Invoked by the search algoritm when a solution has been found. This
* method makes a copy of the puzzle and delivers it to the user's
* {@link cx.prutser.sudoku.solver.SolutionsCollector#newSolution(Object[], SolverContext)}
* callback.
*
* @param collector
* @throws CanceledException when the users canceled the search.
*/
private void reportSolution(SolutionsCollector<Integer> collector)
throws CanceledException {
final AtomicBoolean cancel = new AtomicBoolean(false);
final Integer[] solution = new Integer[81];
final long evaluations = evals;
for (int i = 0; i < 81; i++) {
solution[i] = tiles[i].getValue();
}
collector.newSolution(solution, new SolverContext() {
public void cancel() {
cancel.set(true);
}
public long evaluations() {
return evaluations;
}
});
if(cancel.get()) {
throw new CanceledException();
}
}
/**
* Used internally by this solver to signal that the search must stop
* because the user called {@link cx.prutser.sudoku.solver.SolverContext#cancel()}.
*/
private class CanceledException extends Exception {
private CanceledException() {
}
}
}