Coursera Algorithm(Part I) Week4 Assignment 8 Puzzle (98/100)
跟著學習Algorithm的課程也有好幾周了,感覺作業難度還算適中,但要想得滿分實在需要一些姿勢水平。對演算法時間和記憶體的高階要求對我這個外行簡直吃力,從第三週開始就已經不能100/100了,又苦於不知道向誰求救,很難受。加上自己記性一直不好,一些之前理解很到位的演算法隔一段時間不用就只能記個模稜兩可,所以決定把作業發到網上(前三週的有時間再補),一方面希望和大家分享交流,聽聽大家的建議(如果有人看的話[手動捂臉.jpg]),一方面也給自己留個備案,糊塗時就翻出來看看。(希望沒有違反honor code...)
1.Specification
8 Puzzle
Write a program to solve the 8-puzzle problem (and its natural generalizations) using the A* search algorithm.
The problem. The 8-puzzle problem is a puzzle invented and popularized by Noyes Palmer Chapman in the 1870s. It is played on a 3-by-3 grid with 8 square blocks labeled 1 through 8 and a blank square. Your goal is to rearrange the blocks so that they are in order, using as few moves as possible. You are permitted to slide blocks horizontally or vertically into the blank square. The following shows a sequence of legal moves from an initial board
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
Best-first search. Now, we describe a solution to the problem that illustrates a general artificial intelligence methodology known as the
- Hamming priority function. The number of blocks in the wrong position, plus the number of moves made so far to get to the search node. Intuitively, a search node with a small number of blocks in the wrong position is close to the goal, and we prefer a search node that have been reached using a small number of moves.
- Manhattan priority function. The sum of the Manhattan distances (sum of the vertical and horizontal distance) from the blocks to their goal positions, plus the number of moves made so far to get to the search node.
8 1 3 1 2 3 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 4 2 4 5 6 ---------------------- ---------------------- 7 6 5 7 8 1 1 0 0 1 1 0 1 1 2 0 0 2 2 0 3 initial goal Hamming = 5 + 0 Manhattan = 10 + 0
We make a key observation: To solve the puzzle from a given search node on the priority queue, the total number of moves we need to make (including those already made) is at least its priority, using either the Hamming or Manhattan priority function. (For Hamming priority, this is true because each block that is out of place must move at least once to reach its goal position. For Manhattan priority, this is true because each block must move its Manhattan distance from its goal position. Note that we do not count the blank square when computing the Hamming or Manhattan priorities.) Consequently, when the goal board is dequeued, we have discovered not only a sequence of moves from the initial board to the goal board, but one that makes the fewest number of moves. (Challenge for the mathematically inclined: prove this fact.)
A critical optimization. Best-first search has one annoying feature: search nodes corresponding to the same board are enqueued on the priority queue many times. To reduce unnecessary exploration of useless search nodes, when considering the neighbors of a search node, don't enqueue a neighbor if its board is the same as the board of the previous search node.
8 1 3 8 1 3 8 1 8 1 3 8 1 3 4 2 4 2 4 2 3 4 2 4 2 5 7 6 5 7 6 5 7 6 5 7 6 5 7 6 previous search node neighbor neighbor neighbor (disallow)
Game tree. One way to view the computation is as a game tree, where each search node is a node in the game tree and the children of a node correspond to its neighboring search nodes. The root of the game tree is the initial search node; the internal nodes have already been processed; the leaf nodes are maintained in a priority queue; at each step, the A* algorithm removes the node with the smallest priority from the priority queue and processes it (by adding its children to both the game tree and the priority queue).
Detecting unsolvable puzzles. Not all initial boards can lead to the goal board by a sequence of legal moves, including the two below:
1 2 3 1 2 3 4 4 5 6 5 6 7 8 8 7 9 10 11 12 13 15 14 unsolvable unsolvableTo detect such situations, use the fact that boards are divided into two equivalence classes with respect to reachability: (i) those that lead to the goal board and (ii) those that lead to the goal board if we modify the initial board by swapping any pair of blocks (the blank square is not a block). (Difficult challenge for the mathematically inclined: prove this fact.) To apply the fact, run the A* algorithm on two puzzle instances—one with the initial board and one with the initial board modified by swapping a pair of blocks—in lockstep (alternating back and forth between exploring search nodes in each of the two game trees). Exactly one of the two will lead to the goal board.
Board and Solver data types. Organize your program by creating an immutable data type Board with the following API:
public class Board {
public Board(int[][] blocks) // construct a board from an n-by-n array of blocks
// (where blocks[i][j] = block in row i, column j)
public int dimension() // board dimension n
public int hamming() // number of blocks out of place
public int manhattan() // sum of Manhattan distances between blocks and goal
public boolean isGoal() // is this board the goal board?
public Board twin() // a board that is obtained by exchanging any pair of blocks
public boolean equals(Object y) // does this board equal y?
public Iterable<Board> neighbors() // all neighboring boards
public String toString() // string representation of this board (in the output format specified below)
public static void main(String[] args) // unit tests (not graded)
}
Corner cases. You may assume that the constructor receives an n-by-n array containing the n2 integers between 0 and n2 − 1, where 0 represents the blank square.
Performance requirements. Your implementation should support all Board methods in time proportional to n2 (or better) in the worst case.
Also, create an immutable data type Solver with the following API:
public class Solver {
public Solver(Board initial) // find a solution to the initial board (using the A* algorithm)
public boolean isSolvable() // is the initial board solvable?
public int moves() // min number of moves to solve initial board; -1 if unsolvable
public Iterable<Board> solution() // sequence of boards in a shortest solution; null if unsolvable
public static void main(String[] args) // solve a slider puzzle (given below)
}
To implement the A* algorithm, you must use MinPQ from algs4.jar for the priority queue(s).
Corner cases. The constructor should throw a java.lang.NullPointerException if passed a null argument.
Solver test client. Use the following test client to read a puzzle from a file (specified as a command-line argument) and print the solution to standard output.
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);
}
}
Input and output formats. The input and output format for a board is the board dimension n followed by the n-by-n initial board, using 0 to represent the blank square. As an example,
% more puzzle04.txt 3 0 1 3 4 2 5 7 8 6 % java Solver puzzle04.txt Minimum number of moves = 4 3 0 1 3 4 2 5 7 8 6 3 1 0 3 4 2 5 7 8 6 3 1 2 3 4 0 5 7 8 6 3 1 2 3 4 5 0 7 8 6 3 1 2 3 4 5 6 7 8 0
% more puzzle3x3-unsolvable.txt 3 1 2 3 4 5 6 8 7 0 % java Solver puzzle3x3-unsolvable.txt No solution possibleYour program should work correctly for arbitrary n-by-n boards (for any 2 ≤ n < 128), even if it is too slow to solve some of them in a reasonable amount of time.
Deliverables. Submit only the files Board.java and Solver.java (with the Manhattan priority). We will supply algs4.jar. You may not call any library functions other those in java.lang, java.util, andalgs4.jar. You must use MinPQ for the priority queue(s).
2.思路
基本思路specification裡已經給的比較詳細了,主要就是利用A*演算法進行最優路徑的選取,中間利用Priority Queue進行高效的排序篩選,資料結構層次為:
- Game Tree
- search node
- Board
Solver.java重點在建構函式,要求實現A*演算法。這裡需要提一點的是,在使用MinPQ時,需要自己新增Comparator作為Board排優先順序的依據。
debug後提交,correctness是沒什麼問題,memory和timing就很慘淡了,還好老師給了checklist,那就照著改唄。
3. Tips
checklist中給了很多建議,我這裡親測有效的有下面幾個:
- 構造Board時用char[] 代替int[][],道理checklist裡面講的很明白,實現起來思路也不麻煩,就是程式碼會多一些強制型別轉換和/,%。
- toString 方法用StringBuilder,不要用+,看來從python裡帶出的習慣要改一改了
- Solver.java 中使用一個MinPQ來儲存origin Board和twin Board。原理checklist裡面沒講,個人感覺可能是兩個twins board中有解的那個的priority至少不會比無解的大,這樣迴圈幾輪後應該就只會pop有解的board了。沒有嚴格的數學證明做支撐,純是個人感覺。至於怎麼只用一個PQ一開始也沒想通,後來搜尋到了一位同學的帖子http://blog.csdn.net/liuweiran900217/article/details/19818289#t4,參考著完成了。
4.程式碼
/**
* Created by zhqch on 2017/2/25.
* Board class for 8puzzle problem in assignment 4: Priority Queue
*/
public class Board {
private char[] blocks;
private int dim;
public Board(int[][] blocks) {
// construct a board from an n-by-n array of blocks
// (where blocks[i][j] = block in row i, column j)s
// save as 1d array to speed up the algorithm
// use char[] instead of int[] to save memory
this.dim = blocks.length;
int n = this.dim * this.dim;
this.blocks = new char[n];
for (int i = 0; i < n; i++) {
this.blocks[i] = (char) blocks[i / dim][i % dim];
}
}
public int dimension() { // board dimension n
return this.dim;
}
public int hamming() { // number of blocks out of place
int hamming = 0;
for (int i = 0; i < this.dim * this.dim; i++) { // don't count blank block
if (blocks[i] != (char) (i + 1) && blocks[i] != (char) 0) {
hamming++;
}
}
return hamming;
}
public int manhattan() { // sum of Manhattan distances between blocks and goal
int manhattan = 0;
for (int i = 0; i < this.dim * this.dim; i++) {
if (blocks[i] != (char) (i + 1) && blocks[i] != (char) 0) { // don't count blank block
int block_val = (int) blocks[i] - 1;
manhattan += Math.abs(block_val / this.dim - i / this.dim) + Math.abs(block_val % this.dim - i % this.dim);
}
}
return manhattan;
}
public boolean isGoal() { // is this board the goal board?
return this.hamming() == 0;
}
public Board twin() { // a board that is obtained by exchanging any pair of blocks
if (blocks[0] == (char) 0 || blocks[1] == (char) 0)
return swap(2, 3);
else
return swap(0, 1);
}
private Board swap(int i, int j) { // swap two blocks of index i and j
int[][] newBlocks = new int[this.dim][this.dim];
for (int k = 0; k < this.dim * this.dim; k++) {
newBlocks[k / this.dim][k % this.dim] = (int) this.blocks[k];
}
int temp = newBlocks[i / this.dim][i % this.dim];
newBlocks[i / this.dim][i % this.dim] = newBlocks[j / this.dim][j % this.dim];
newBlocks[j / this.dim][j % this.dim] = temp;
return new Board(newBlocks);
}
public boolean equals(Object y) { // does this board equal y?
if (y == this) return true;
if (y == null) return false;
if (y.getClass() != this.getClass()) return false;
Board that = (Board) y;
if (that.dimension() != this.dimension()) return false;
for (int i = 0; i < this.dim * this.dim; i++) {
if (this.blocks[i] != that.blocks[i])
return false;
}
return true;
}
public Iterable<Board> neighbors() { // all neighboring boards
Queue<Board> neighborQueue = new Queue<Board>();
int blankPos = 0;
// find the position of blank block
for (int i = 0; i < this.dim * this.dim; i++) {
if (this.blocks[i] == (char) 0) {
blankPos = i;
break;
}
}
if (blankPos / this.dim != 0)
neighborQueue.enqueue(swap(blankPos, blankPos - this.dim));
if (blankPos / this.dim != this.dim - 1)
neighborQueue.enqueue(swap(blankPos, blankPos + this.dim));
if (blankPos % this.dim != 0)
neighborQueue.enqueue(swap(blankPos, blankPos - 1));
if (blankPos % this.dim != this.dim - 1)
neighborQueue.enqueue(swap(blankPos, blankPos + 1));
return neighborQueue;
}
public String toString() { // string representation of this board (in the output format specified below)
StringBuilder output = new StringBuilder(""); //use StringBuilder to save time
output.append(this.dim);
output.append("\n");
for (int i = 0; i < this.dim * this.dim; i++) {
output.append ((int) blocks[i]);
output.append(" ");
if (i % this.dim == this.dim - 1)
output.append("\n");
}
return output.toString();
}
public static void main(String[] args) { // unit tests (not graded)
// int[][] test = {{0, 1, 3},{4, 2, 5},{7 ,8 ,6}};
// Board testBoard2 = new Board(test);
// // read in the board specified in the filename
// In in = new In(args[0]);
// int n = in.readInt();
// int[][] tiles = new int[n][n];
// for (int i = 0; i < n; i++) {
// for (int j = 0; j < n; j++) {
// tiles[i][j] = in.readInt();
// }
// }
// Board testBoard = new Board(tiles);
// StdOut.println(testBoard);
// StdOut.println(testBoard.swap(1, 2));
// StdOut.println(testBoard.twin());
// StdOut.println(testBoard.hamming());
// StdOut.println(testBoard.manhattan());
// StdOut.println(testBoard.isGoal());
// StdOut.println(testBoard.neighbors());
// StdOut.println(testBoard.equals(testBoard2));
}
}
import edu.princeton.cs.algs4.*;
import edu.princeton.cs.algs4.Stack;
import java.util.*;
/**
* Created by Cifer Zhang on 2017/2/27.
*/
public class Solver {
private class SearchNode{ // SearchNode for A* algorithm tree
public Board board;
public int moves;
public SearchNode parent;
public boolean isTwin;
public SearchNode(Board board, int moves, SearchNode parent, boolean isTwin) {
this.board = board;
this.moves = moves;
this.parent = parent;
this.isTwin = isTwin;
}
}
private SearchNode sN;
private int moves = 0;
private boolean solvable = false;
// use Comparator out of class, use Comparable in class
private MinPQ<SearchNode> minPQ = new MinPQ<>(new Comparator<SearchNode>(){
public int compare(SearchNode s1, SearchNode s2){
return s1.board.manhattan() + s1.moves - s2.board.manhattan() - s2.moves;
}
});
private Stack<Board> solutionStack = new Stack<>();
public Solver(Board initial) { // find a solution to the initial board (using the A* algorithm)
minPQ.insert(new SearchNode(initial, 0, null, false));
minPQ.insert(new SearchNode(initial.twin(), 0, null, true));
while(true) {
this.sN = minPQ.delMin();
// terminate search when a goal searchNode dequeue
if (sN.board.isGoal()){
if (sN.isTwin)
this.moves = -1;
else {
this.solvable = true;
this.moves = sN.moves;
// trace back its parent boards as solution
solutionStack.push(sN.board);
while (sN.parent != null) {
sN = sN.parent;
solutionStack.push(sN.board);
}
}
break;
}
for (Board neighbor: sN.board.neighbors()){
if (sN.parent == null || !neighbor.equals(sN.parent.board)) // don't enqueue a visited board
minPQ.insert(new SearchNode(neighbor, sN.moves + 1, sN, sN.isTwin));
}
}
}
public boolean isSolvable() { // is the initial board solvable?
return this.solvable;
}
public int moves() { // min number of moves to solve initial board; -1 if unsolvable
return this.moves;
}
public Iterable<Board> solution() { // sequence of boards in a shortest solution; null if unsolvable
if (this.solvable)
return solutionStack;
else
return null;
}
public static void main(String[] args) {// solve a slider puzzle (given below)
// 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);
}
}
}
5.結果
最後得分是98/100,有三個memory測試通過無望,也就沒再細改了。如果大家有什麼好的改進想法還希望多多提出來,先謝過了。
Computing memory of Solver
*-----------------------------------------------------------
Running 3 total tests.
Test 1: memory with puzzle20.txt (must be <= 2.0x reference solution)
- memory of student Solver = 108568 bytes
- memory of reference Solver = 4896 bytes
- student / reference = 22.17
==> FAILED
Test 2: memory with puzzle25.txt (must be <= 2.0x reference solution)
- memory of student Solver = 1387704 bytes
- memory of reference Solver = 6056 bytes
- student / reference = 229.15
==> FAILED
Test 3: memory with puzzle30.txt (must be <= 2.0x reference solution)
- memory of student Solver = 5719512 bytes
- memory of reference Solver = 7216 bytes
- student / reference = 792.62
==> FAILED
Total: 0/3 tests passed!