8 puzzle
使用A*搜索算法解决8-puzzle问题。
Board.java
Board类用来表示一个
n∗n
n
∗
n
的网格,其中有
n2−1
n
2
−
1
个方块,每个从1标记到
n2−1
n
2
−
1
,还有一个方块是空。
这里提供了计算Hamming距离和Manhattan距离的方法。也提供了计算其“双胞胎”Board的方法。
import java.util.Arrays;
import edu.princeton.cs.algs4.In;
import edu.princeton.cs.algs4.Stack;
import edu.princeton.cs.algs4.StdOut;
/**
* The {@code Board} class creates an immutable data type to
* represent a n-by-n grid with n^2-1 square blocks labeled 1
* through n^2-1 and a blank square.
* <p>
* It supports operations of calculating the Hamming and
* Manhattan priorities, along with methods for iterating
* through neighbors of the Board, determining whether the
* Board is a goal, and getting its twin Board.
*
* @author zhangyu
* @date 2017.3.29
*/
public class Board
{
private int[] blocks1D; // 1D array of blocks
private int n; // dimension of the Board
/**
* Constructs a board from an n-by-n array of blocks
* (where blocks[i][j] = block in row i, column j).
*
* @param blocks the initial n-by-n array of blocks
* @throws NullPointerException if the given array is null
*/
public Board(int[][] blocks)
{
if (blocks == null) throw new NullPointerException("Null blocks");
n = blocks.length;
this.blocks1D = new int[n * n];
for (int i = 0; i < n; ++i) // copy the given 2D array to a 1D.
for (int j = 0; j < n; ++j)
this.blocks1D[i * n + j] = blocks[i][j];
}
// private constructor with a 1D array as a parameter
private Board(int[] blocks)
{
n = (int) Math.sqrt(blocks.length);
this.blocks1D = Arrays.copyOf(blocks, blocks.length);
}
/**
* Returns dimension of the board.
*
* @return dimension of the board
*/
public int dimension()
{
return n;
}
/**
* Returns number of blocks in the wrong position.
*
* @return number of blocks in the wrong position
*/
public int hamming()
{
int hamming = 0;
for (int i = 0; i < blocks1D.length; ++i) // ignore the blank square
if (blocks1D[i] != 0 && blocks1D[i] != i + 1)
hamming++;
return hamming;
}
/**
* Returns sum of Manhattan distances between blocks and goal.
*
* @return sum of Manhattan distances between blocks and goal
*/
public int manhattan()
{
int manhattan = 0;
for (int i = 0; i < blocks1D.length; ++i) // ignore the blank square
if (blocks1D[i] != 0 && blocks1D[i] != i + 1)
manhattan += Math.abs((blocks1D[i] - 1) / n - i / n) + // distance of rows
Math.abs((blocks1D[i] - 1) % n - i % n); // distance of columns
return manhattan;
}
/**
* Determine whether this board is the goal board.
*
* @return true if the board is goal board.
* false otherwise
*/
public boolean isGoal()
{
return this.hamming() == 0;
}
/**
* Returns a board that is obtained by exchanging any pair of
* initial blocks(except the blank square).
*
* @return a board with exchanged blocks
*/
public Board twin()
{
int[] twinBlocks;
if (blocks1D[0] != 0 && blocks1D[1] != 0)
twinBlocks = getSwappedBlocks(0, 1);
else
twinBlocks = getSwappedBlocks(n * n - 2, n * n - 1);
return new Board(twinBlocks);
}
/**
* Determine if this board equals to y?
*
* @return true if this equals to y.
* false otherwise
*/
public boolean equals(Object y)
{
if (y == this) return true;
if (y == null) return false;
if (y.getClass() != this.getClass()) return false;
Board that = (Board) y;
if (this.dimension() != that.dimension()) return false;
return Arrays.equals(this.blocks1D, that.blocks1D);
}
/**
* Returns an Iterable data type containing all neighboring boards.
*
* @return an Iterable data type containing all neighboring boards
*/
public Iterable<Board> neighbors()
{
Stack<Board> neighbors = new Stack<Board>();
int[] xDiff = {-1, 1, 0, 0};
int[] yDiff = {0, 0, -1, 1};
int[] swappedBlocks;
int idxOfBlank; // position of the blank square
int idxOfNB; // position of a neighbor
// find position of the blank square
for (idxOfBlank = 0; idxOfBlank < blocks1D.length; ++idxOfBlank)
if (blocks1D[idxOfBlank] == 0) break;
for (int i = 0; i < 4; ++i)
{
int rowOfNB = idxOfBlank / n + xDiff[i];
int colOfNB = idxOfBlank % n + yDiff[i];
if (rowOfNB >= 0 && rowOfNB < n && colOfNB >= 0 && colOfNB < n)
{
idxOfNB = rowOfNB * n + colOfNB;
swappedBlocks = getSwappedBlocks(idxOfBlank, idxOfNB);
neighbors.push(new Board(swappedBlocks));
}
}
return neighbors;
}
// swaps elements of blocks1D and returns a new array
private int[] getSwappedBlocks(int i, int j)
{
// copy the blocks
int[] blocks = Arrays.copyOf(blocks1D, blocks1D.length);
int swap = blocks[i];
blocks[i] = blocks[j];
blocks[j] = swap;
return blocks;
}
/**
* string representation of this board (in the output format specified below)
*
* @return a String representing this board
*/
public String toString()
{
StringBuilder board = new StringBuilder();
board.append(n + "\n");
for (int i = 0; i < blocks1D.length; i++)
{
board.append(String.format("%2d ", blocks1D[i]));
if ((i + 1) % n == 0) board.append("\n");
}
return board.toString();
}
/**
* Unit tests the {@code Board} data type.
*
* @param args the command-line arguments
*/
public static void main(String[] args)
{
In in = new In(args[0]);
int n = in.readInt();
int[][] blocks = new int[n][n];
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
blocks[i][j] = in.readInt();
Board initial = new Board(blocks);
StdOut.println(initial.dimension());
StdOut.println(initial.toString());
StdOut.println(initial.hamming());
StdOut.println(initial.manhattan());
StdOut.println(initial.twin().toString());
for (Board nb : initial.neighbors())
for (Board nbb : nb.neighbors())
StdOut.println(nbb.toString());
}
}
Solver.java
Solver给了8 puzzle的解决办法,所谓8 puzzle,就是给定一个初始的
3∗3
3
∗
3
网格,从1到8标记,一个为空(就是Board的实现),通过合法的变换,以最少的移动次数是这个网格达到有序。(还有15 puzzle问题,说的一样的东西)
就像下面的实现。
1 3 1 3 1 2 3 1 2 3 1 2 3
4 2 5 => 4 2 5 => 4 5 => 4 5 => 4 5 6
7 8 6 7 8 6 7 8 6 7 8 6 7 8
initial 1 left 2 up 5 left goal
解决办法是A*算法,通过一个优先队列实现。
优先队列要求对象可比较,而Board没有实现compareTo()
方法,所以可以把Board放入一个私有类SearchNode中,SearchNode实现compareTo()
方法。
实现前有一个可解性问题,就像下面这样的:
1 2 3 1 2 3 4
4 5 6 5 6 7 8
8 7 9 10 11 12
13 15 14
unsolvable
unsolvable
这样的初始网格是不可解的。
当然这是特例,一般化的结论是,若初始网格的逆序数(在一个排列中,如果一对数的前后位置与大小顺序相反,即前面的数大于后面的数,那么它们就称为一个逆序,所有的逆序个数就是逆序数)与目标逆序数的奇偶性不同,则这个初始网格是不可解的。
然而由于没有提供返回blocks数组的API,所以计算逆序数及其奇偶性也就无从谈起,只能另辟蹊径。这里还有个结论:一个初始排列,若将其任意两个位置的值(除了那个空白,与空白交换不改变奇偶性)交换,那么奇偶性就会改变。所以一个Board和它的双胞胎的奇偶性必然是不同的,而且,这两者必然有一个的奇偶性与目标的一样,即必然有且只有一个可解。如果双胞胎可解,那么初始Board就不可解,反之亦然。所以可以创建两个优先队列,分别加入初始Board和双胞胎,同时移动,如果一个先达到目标,那么另一个就是不可解的。作为优化,可以只创建一个优先队列,加入初始Board和双胞胎,这个队列肯定有解,但是要判断是哪个导致的有解(是初始Board还是双胞胎),可以在SearchNode里放一个标记位isInitParity
以表明是否是和初始Board的奇偶性一样,加入的新搜索结点继承删除的搜索结点的奇偶性,最终通过不断的移动达到目标。这时候看看目标的奇偶性,即isInitParity
是否为真,真就是说明和初始的一样的奇偶性,那么就是初始Board达到了目标,可解,否则不可解。
实现的时候有一个提醒,那就是不要把优先队列作为成员变量,否则内存使用会爆炸。
作为对比:
不作为成员变量:
Test 3: memory with puzzle30.txt (must be <= 2.0x reference solution)
- memory of student Solver = 4744 bytes
- memory of reference Solver = 7216 bytes
- student / reference = 0.66 ==> passed
作为成员变量:
Test 3: memory with puzzle30.txt (must be <= 2.0x reference solution)
- memory of student Solver = 10092328 bytes
- memory of reference Solver = 7216 bytes
- student / reference = 1398.60 ==> FAILED
差别就是这么明显。
还有用一维数组代替二维数组也会改善空间的使用。
import edu.princeton.cs.algs4.In;
import edu.princeton.cs.algs4.MinPQ;
import edu.princeton.cs.algs4.Stack;
import edu.princeton.cs.algs4.StdOut;
/**
* The {@code Solver} class provides solution for the
* 8-puzzle problem (and its natural generalizations)
* using the A* search algorithm.
*
* @author zhangyu
* @date 2017.3.29
*/
public class Solver
{
private SearchNode current; // current search node
private boolean isSolvable;
// the class implements the compareTo() method for MinPQ
private class SearchNode implements Comparable<SearchNode>
{
private Board bd;
private SearchNode previous;
private int moves;
private int priority; // cache the priority to avoid repeated calculations
private boolean isInitParity; // is equal to initial parity
// this constructor is used for initail node and its twin
public SearchNode(Board bd, boolean isInitParity)
{
if (bd == null)
throw new NullPointerException("Null Board");
this.bd = bd;
this.moves = 0;
this.priority = this.bd.manhattan() + this.moves;
this.isInitParity = isInitParity;
}
// for the later nodes
public SearchNode(Board bd, SearchNode previous)
{
if (bd == null)
throw new NullPointerException("Null Board");
if (previous == null)
throw new NullPointerException("Null SearchNode");
this.bd = bd;
this.previous = previous;
this.moves = previous.moves + 1;
this.priority = this.bd.manhattan() + this.moves;
this.isInitParity = previous.isInitParity;
}
public int compareTo(SearchNode that)
{
// When two search nodes have the same Manhattan priority,
// break ties by comparing the Manhattan distances of the two boards.
if (this.priority == that.priority)
return this.bd.manhattan() - this.bd.manhattan();
else
return this.priority - that.priority;
}
}
/**
* Find a solution to the initial board (using the A* algorithm).
*
* @param initial the initial Board
* @throws NullPointerException if initial Board is null
*/
public Solver(Board initial)
{
if (initial == null) throw new NullPointerException("Null Board");
MinPQ<SearchNode> origPQ = new MinPQ<SearchNode>();
// isInitParity of initial is true, and another is false.
origPQ.insert(new SearchNode(initial, true)); // insert initial node and its twin
origPQ.insert(new SearchNode(initial.twin(), false));
while (true)
{
current = origPQ.delMin();
if (current.bd.isGoal()) break;
for (Board nb : current.bd.neighbors())
if (current.previous == null ||
!nb.equals(current.previous.bd))
origPQ.insert(new SearchNode(nb, current));
} // only one of the two nodes can lead to the goal board
isSolvable = current.isInitParity && current.bd.isGoal();
}
/**
* Determines whether the initial board is solvable?
*
* @return true if the initial board is solvable;
* false otherwise.
*/
public boolean isSolvable()
{
return this.isSolvable;
}
/**
* Returns min number of moves to solve initial board; -1 if unsolvable.
*
* @return min number of moves to solve initial board; -1 if unsolvable
*/
public int moves()
{
if (!isSolvable()) return -1;
else return current.moves;
}
/**
* Returns sequence of boards in a shortest solution; null if unsolvable.
*
* @return sequence of boards in a shortest solution;
* null if unsolvable.
*/
public Iterable<Board> solution()
{
if (!isSolvable()) return null;
Stack<Board> boards = new Stack<Board>();
SearchNode node = current; // another reference
while (node != null)
{
boards.push(node.bd);
node = node.previous;
}
return boards;
}
/**
* Unit tests the {@code solver} data type to
* solve a slider puzzle (given below).
*
* @param args the command-line arguments
*/
public static void main(String[] args)
{
// create initial board from file
In in = new In(args[0]);
int n = in.readInt();
int[][] blocks = new int[n][n];
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
blocks[i][j] = in.readInt();
Board initial = new Board(blocks);
// solve the puzzle
Solver solver = new Solver(initial);
// print solution to standard output
if (!solver.isSolvable())
StdOut.println("No solution possible");
else
{
StdOut.println("Minimum number of moves = " + solver.moves());
for (Board board : solver.solution())
StdOut.println(board);
}
}
}
ASSESSMENT SUMMARY
Compilation: PASSED
API: PASSED
Findbugs: PASSED
Checkstyle: PASSED
Correctness: 42/42 tests passed
Memory: 11/11 tests passed
Timing: 17/17 tests passed
Aggregate score: 100.00%
[Compilation: 5%, API: 5%, Findbugs: 0%, Checkstyle: 0%, Correctness: 60%, Memory: 10%, Timing: 20%]