That's gonna take long.
This program solves 8 puzzle game (mini version of 15 puzzle) using A* algorithm. Program consists of 2 parts:
Board.java
- which serves as a representation for the board (N-by-N grid, not limited to 9 tiles)Solver.java
- as the name implies, this class solves the given puzzle (if it is solvable)
Here is the sample 3-by-3 Board (taken from toString()
):
4 1 3
0 2 6
7 5 8
One issue with this code is that it is painfully slow (I realize that majority of methods take quadratic time, however this is way slower than the spec requires it to be). The problem lies here:
Output from Solver
class (# of moves == number of expected moves, time measured in seconds)
# of moves = 5 && # of actual moves 5 & time passed 0.004000
# of moves = 7 && # of actual moves 35 & time passed 0.002000
# of moves = 8 && # of actual moves 9 & time passed 0.000000
# of moves = 9 && # of actual moves 9 & time passed 0.001000
# of moves = 11 && # of actual moves 260 & time passed 0.008000
# of moves = 18 && # of actual moves 6560 & time passed 0.056000
# of moves = 25 && # of actual moves 267431 & time passed 3.963000
I have commented the one with 36 required moves in Solver
class's main
method, cause it may consume a lot of memory and slow down your machine. Make sure you run from the command line; it perfroms poorly from Eclipse
This code is doing a lot of unnecessary work (basically walking in a circles) until it realizes that this was all waste and only then takes optimal path. You can see it by comparing number of expected moves vs. number of actual moves. My assumption was corroborated by careful debugging session and the problem is that I don't know how to circumvent this issue. Maybe insight into priority function will help: There are 2 of them, Hamming and Manhattan (Manhattan is preferred, cause it converges faster) Given the board below, Manhattan is calculated by subtracting deviations of tile from their positions:
8 1 3
4 2
7 6 5
For example, tile 8 (the first one) is 3 tiles away from its expected position (2 rows down, 1 column to the right) Tile 1 (the second one) is 1 tile away from its position (1 column to the left)
If we consider the board as a list, then we can calculate Manhattan distance in the following way:
1 2 3 4 5 6 7 8
---------------------- Manhattan = 1 + 2 + 2 + 2 + 3 = 10
1 2 0 0 2 2 0 3
Each number in the top, indicates how the board should be aligned, to be deemed as solved, and numbers below, indicating how many tiles are these numbers away from their expected position. Basically, Manhattan priority function will show us the minimum number of moves are needed to solve the puzzle.
Now priority function of the Board is calculated as its Manhattan distance + number of moves made to reach to that very state. For instance, for the inserted Board, priority will be equal to its Manhattan distance + 0 moves, since no steps were made. For the next neighbor node, which is dequeued, moves will equal to 1 and so on.
So what basically A* star does, it looks at the neighbors of the current Board (+-1 row, +-1 col), dequeues it (of course saving it) and looks for the neighbors of the dequeued Board and so on, until dequeued Board is the goal Board (A* is guaranteed to converge).
You see, when we have two boards with the same Manhattan priorities, PriorityQueue chooses the first one even though it leads to a bad, I would even say catastrophic consequences and surely, as complexity of the puzzle increases, these fallacies add up (the thing that is infinitely obvious to us, humans, doesn't seem to be so for computers) Alternatively, algorithm checks previous Boards, that are stored in PQ (with lower priority functions, due to the small number of moves) and after checking them, without any result to show up, it chooses the right one.
- Here is the spec (I have swapped MinPQ with Java's Priority Queue implementation): http://www.cs.princeton.edu/courses/archive/fall15/cos226/assignments/8puzzle.html
- Here is the checklist that might address your questions: http://www.cs.princeton.edu/courses/archive/fall15/cos226/checklist/8puzzle.html
- Here are the data files you can use: ftp://ftp.cs.princeton.edu/pub/cs226/8puzzle/
Here comes JUnit 4 tests for both classes:
https://gist.github.com/jmstfv/633f924e28f6f3440841ef2c75381d88 (This one uses Apache Commons library)
https://gist.github.com/jmstfv/a717b24761b08bb7d80c9950b621cb27
Here comes the code for 2 classes. P.S Sorry for Javadocs, I couldn't complete them Board.java
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
public final class Board {
private final int[][] tilesCopy;
private final int N;
// cache
private int hashCode = -1;
private int zeroRow = -1;
private int zeroCol = -1;
private Collection<Board> neighbors;
/*
* Rep Invariant
* tilesCopy.length > 0
* Abstraction Function
* represent single board of 8 puzzle game
* Safety Exposure
* all fields are private and final (except cache variables). In the constructor,
* defensive copy of tiles[][] (array that is received from the client)
* is done.
*/
public Board(int[][] tiles) {
this.N = tiles.length;
this.tilesCopy = new int[N][N];
// defensive copy
for (int i = 0; i < N; i++) {
for (int j = 0; j < N; j++) {
if (tiles[i][j] >= 0 && tiles[i][j] < N*N) tilesCopy[i][j] = tiles[i][j];
else {
System.out.printf("Illegal tile value at (%d, %d): "
+ "should be between 0 and N^2 - 1.", i, j);
System.exit(1);
}
}
}
checkRep();
}
public int tileAt(int row, int col) {
if (row < 0 || row > N - 1) throw new IndexOutOfBoundsException
("row should be between 0 and N - 1");
if (col < 0 || col > N - 1) throw new IndexOutOfBoundsException
("col should be between 0 and N - 1");
return tilesCopy[row][col];
}
public int size() {
return N;
}
public int hamming() {
int hamming = 0;
for (int row = 0; row < this.size(); row++) {
for (int col = 0; col < this.size(); col++) {
if (tileAt(row, col) != 0 && tileAt(row, col) != (row*N + col + 1)) hamming++;
}
}
return hamming;
}
// sum of Manhattan distances between tiles and goal
public int manhattan() {
int manhattan = 0;
int expectedRow = 0, expectedCol = 0;
for (int row = 0; row < this.size(); row++) {
for (int col = 0; col < this.size(); col++) {
if (tileAt(row, col) != 0 && tileAt(row, col) != (row*N + col + 1)) {
expectedRow = (tileAt(row, col) - 1) / N;
expectedCol = (tileAt(row, col) - 1) % N;
manhattan += Math.abs(expectedRow - row) + Math.abs(expectedCol - col);
}
}
}
return manhattan;
}
public boolean isGoal() {
if (tileAt(N-1, N-1) != 0) return false; // prune
for (int i = 0; i < this.size(); i++) {
for (int j = 0; j < this.size(); j++) {
if (tileAt(i, j) != 0 && tileAt(i, j) != (i*N + j + 1)) return false;
}
}
return true;
}
// change i && j' s name
public boolean isSolvable() {
int inversions = 0;
for (int i = 0; i < this.size() * this.size(); i++) {
int currentRow = i / this.size();
int currentCol = i % this.size();
if (tileAt(currentRow, currentCol) == 0) {
this.zeroRow = currentRow;
this.zeroCol = currentCol;
}
for (int j = i; j < this.size() * this.size(); j++) {
int row = j / this.size();
int col = j % this.size();
if (tileAt(row, col) != 0 && tileAt(row, col) < tileAt(currentRow, currentCol)) {
inversions++;
}
}
}
if (tilesCopy.length % 2 != 0 && inversions % 2 != 0) return false;
if (tilesCopy.length % 2 == 0 && (inversions + this.zeroRow) % 2 == 0) return false;
return true;
}
@Override
public boolean equals(Object y) {
if (!(y instanceof Board)) return false;
Board that = (Board) y;
if (this.tileAt(N-1, N-1) != that.tileAt(N-1, N-1)) return false; // why bother checking whole array, if last elements aren't equals
if (this.size() != that.size()) return false;
return Arrays.deepEquals(this.tilesCopy, that.tilesCopy);
}
@Override
public int hashCode() {
if (this.hashCode != -1) return hashCode;
// more optimized version(Arrays.hashCode is too slow)?
this.hashCode = Arrays.deepHashCode(tilesCopy);
return this.hashCode;
}
public Collection<Board> neighbors() {
if (neighbors != null) return neighbors;
if (this.zeroRow == -1 && this.zeroCol == -1) findZeroTile();
neighbors = new HashSet<>();
if (zeroRow - 1 >= 0) generateNeighbor(zeroRow - 1, true);
if (zeroCol - 1 >= 0) generateNeighbor(zeroCol - 1, false);
if (zeroRow + 1 < this.size()) generateNeighbor(zeroRow + 1, true);
if (zeroCol + 1 < this.size()) generateNeighbor(zeroCol + 1, false);
return neighbors;
}
private void findZeroTile() {
outerloop:
for (int i = 0; i < this.size(); i++) {
for (int j = 0; j < this.size(); j++) {
if (tileAt(i, j) == 0) {
this.zeroRow = i; // index starting from 0
this.zeroCol = j;
break outerloop;
}
}
}
}
private void generateNeighbor(int toPosition, boolean isRow) {
int[][] copy = Arrays.copyOf(this.tilesCopy, tilesCopy.length);
if (isRow) swapEntries(copy, zeroRow, zeroCol, toPosition, zeroCol);
else swapEntries(copy, zeroRow, zeroCol, zeroRow, toPosition);
neighbors.add(new Board(copy));
}
private void swapEntries(int[][] array, int fromRow, int fromCol, int toRow, int toCol) {
int i = array[fromRow][fromCol];
array[fromRow][fromCol] = array[toRow][toCol];
array[toRow][toCol] = i;
}
public String toString() {
StringBuilder s = new StringBuilder(4 * N * N); // optimization?
// s.append(N + "\n");
for (int i = 0; i < N; i++) {
for (int j = 0; j < N; j++) {
s.append(String.format("%2d ", tileAt(i, j)));
}
s.append("\n");
}
return s.toString();
}
private void checkRep() {
assert tilesCopy.length > 0;
}
}
Solver.java
import java.util.HashSet;
import java.util.Set;
import java.util.Objects;
import board.Board;
import java.util.Stack;
import java.util.PriorityQueue;
public class Solver {
private final PriorityQueue<SearchNode> minPQ;
private int moves = 0;
private SearchNode finalNode;
private Stack<Board> boards;
/* Rep Invariant
* TODO
* Abstraction Function
* TODO
* Safety Exposure Argument
* TODO
*/
/**
* find a solution to the initial board (using the A* algorithm)
* @param initial
* @throws NullPointerException
* @throws IllegalArgumentException
*/
public Solver(Board initial) {
Objects.requireNonNull("board can't be null");
if (!initial.isSolvable()) throw new IllegalArgumentException("Unsolvable puzzle");
// this.initial = initial;
this.minPQ = new PriorityQueue<SearchNode>(initial.size() + 10); // magic number :>
Set<Board> previouses = new HashSet<Board>(50);
Board dequeuedBoard = initial;
Board previous = null;
SearchNode dequeuedNode = new SearchNode(initial, 0, null);
Iterable<Board> boards;
while (!dequeuedBoard.isGoal()) {
boards = dequeuedBoard.neighbors();
moves++;
for (Board board : boards) {
if (!board.equals(previous) && !previouses.contains(board)) {
minPQ.add(new SearchNode(board, moves, dequeuedNode));
}
}
previouses.add(previous);
previous = dequeuedBoard;
dequeuedNode = minPQ.poll();
dequeuedBoard = dequeuedNode.current;
}
finalNode = dequeuedNode;
}
// min number of moves to solve initial board
public int moves() {
if (boards != null) return boards.size()-1;
solution();
return boards.size() - 1;
}
public Iterable<Board> solution() {
if (boards != null) return boards;
boards = new Stack<Board>();
SearchNode pointer = finalNode;
while (pointer != null) {
boards.push(pointer.current);
pointer = pointer.previous;
}
return boards;
}
private class SearchNode implements Comparable<SearchNode> {
private final int priority;
private final SearchNode previous;
private final Board current;
public SearchNode(Board current, int moves, SearchNode previous) {
this.current = current;
this.previous = previous;
this.priority = moves + current.manhattan();
}
@Override
public int compareTo(SearchNode that) {
int cmp = this.priority - that.priority;
if (cmp < 0) return -1;
else if (cmp > 0) return 1;
else return 0;
}
}
public static void main(String[] args) {
int[][] tiles5 = {{4, 1, 3},
{0, 2, 6},
{7, 5, 8}};
int[][] tiles7 = {{1, 2, 3},
{0, 7, 6},
{5, 4, 8}};
int[][] tiles8 = {{2, 3, 5},
{1, 0, 4},
{7, 8, 6}};
int[][] tiles9 = {{2, 0, 3, 4},
{1, 10, 6, 8},
{5, 9, 7, 12},
{13, 14, 11, 15}};
int[][] tiles11 = {{1, 0, 2},
{7, 5, 4},
{8, 6, 3}};
int[][] tiles18 = {{5, 6, 2},
{1, 8, 4},
{7, 3, 0}};
// answer will be here, compare w/ other
int[][] tiles25 = {{2, 8, 5},
{3, 6, 1},
{7, 0, 4}};
int[][] tiles36 = {{5, 3, 1, 4},
{10, 2, 8, 7},
{14, 13, 0, 11},
{6, 9, 15, 12}};
double start5 = System.currentTimeMillis();
Board board5 = new Board(tiles5);
Solver solve5 = new Solver(board5);
System.out.printf("# of moves = %d && # of actual moves %d & time passed %f\n, ", solve5.moves(), solve5.moves, (System.currentTimeMillis() - start5) / 1000);
double start7 = System.currentTimeMillis();
Board board7 = new Board(tiles7);
Solver solve7 = new Solver(board7);
System.out.printf("# of moves = %d && # of actual moves %d & time passed %f\n", solve7.moves(), solve7.moves, (System.currentTimeMillis() - start7) / 1000);
double start8 = System.currentTimeMillis();
Board board8 = new Board(tiles8);
Solver solve8 = new Solver(board8);
System.out.printf("# of moves = %d && # of actual moves %d & time passed %f\n", solve8.moves(), solve8.moves, (System.currentTimeMillis() - start8) / 1000);
double start9 = System.currentTimeMillis();
Board board9 = new Board(tiles9);
Solver solve9 = new Solver(board9);
System.out.printf("# of moves = %d && # of actual moves %d & time passed %f\n", solve9.moves(), solve9.moves, (System.currentTimeMillis() - start9) / 1000);
double start11 = System.currentTimeMillis();
Board board11 = new Board(tiles11);
Solver solve11 = new Solver(board11);
System.out.printf("# of moves = %d && # of actual moves %d & time passed %f\n", solve11.moves(), solve11.moves, (System.currentTimeMillis() - start11) / 1000);
double start18 = System.currentTimeMillis();
Board board18 = new Board(tiles18);
Solver solve18 = new Solver(board18);
System.out.printf("# of moves = %d && # of actual moves %d & time passed %f\n", solve18.moves(), solve18.moves, (System.currentTimeMillis() - start18) / 1000);
//
double start25 = System.currentTimeMillis();
Board board25 = new Board(tiles25);
Solver solve25 = new Solver(board25);
System.out.printf("# of moves = %d && # of actual moves %d & time passed %f\n", solve25.moves(), solve25.moves, (System.currentTimeMillis() - start25) / 1000);
// double start36 = System.currentTimeMillis();
// Board board36 = new Board(tiles36);
// Solver solve36 = new Solver(board36);
// System.out.printf("# of moves = %d && # of actual moves %d & time passed %f\n", solve36.moves(), solve36.moves, (System.currentTimeMillis() - start36) / 1000);
}
}