第一次跟Princeton的《Algorithm》课没有跟下来的一个核心原因就是这道题无论如何拿不到full score,第一次做的时候分数只有不到85分,因为最后的测试总是有一些test不能通过。经过了大概6个月后,现在再编这个题目,结果还是拿不到full score,不过分数大约提高了10分,只有部分的test不能通过。然而,我自己还是找不到为何通不过的原因,因此先把现在的代码贴出来,和大家分享,也希望朋友们一起帮忙找找错误…
Write a program to solve the 8-puzzle problem(and its natural generalizations) using the A* search algorithm.
The problem.The 8-puzzle problemis 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, usingas few moves as possible. You are permitted to slide blocks horizontally or verticallyinto the blank square. The following shows a sequence of legal moves from an initial board (left)to the goal board (right).
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 theA* search algorithm.We define a search node of the game to be a board, the numberof moves made to reach the board, and the previous search node.First, insert the initial search node(the initial board, 0 moves, and a null previous search node) into a priority queue. Then,delete from the priority queue the search node with the minimum priority,and insert onto the priority queue all neighboring search nodes(those that can be reached in one move from the dequeued search node).Repeat this procedure until the search node dequeued corresponds to a goal board.The success of this approachhinges on the choice of priority function for a search node. We consider two priority functions:
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 oberservation: To solve the puzzle froma given search node on the priority queue, the total number of moves weneed 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 placemust move at least once to reach its goal position.For Manhattan priority, this is true because each block must moveits Manhattan distance from its goal position.Note that we do not count the blank square when computing theHamming or Manhattan priorities.)Consequently, when the goal board is dequeued, wehave discovered not only a sequence of moves from theinitial 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 boardare 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 enqueuea neighbor if its board is the same as the board of theprevious 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 nodeis a node in the game tree and the children of a node correspond to itsneighboring search nodes. The root of the game tree is the initial search node;the internal nodes have already been processed; the leaf nodes are maintainedin a priority queue; at each step, the A* algorithm removes the node with the smallestpriority from the priority queue and processes it (by adding its childrento both the game tree and the priority queue).
Detecting infeasible puzzles.Not all initial boards can lead to the goal board such as the one below.
1 2 3 4 5 6 8 7 infeasibleTo detect such situations,use the fact that boardsare divided into two equivalence classes with respect to reachability:(i) those that lead to the goal board and (ii) those thatlead to the goal board if we modify the initial board byswapping any pair of adjacent (non-blank) blocks in the same row.(Difficult challenge for the mathematically inclined: prove this fact.)To apply the fact,run the A* algorithm simultaneously on two puzzle instances—one with theinitial board and one with the initial board modified byswapping a pair of adjacent blocks in the same row. Exactly one ofthe 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 obtained by exchanging two adjacent blocks in the same row public boolean equals(Object y) // does this board equal y? public Iterableneighbors() // all neighboring boards public String toString() // string representation of the board (in the output format specified below) }
and an immutable data type Solver with the following API:
To implement the A* algorithm, you must use the MinPQ data type from algs4.jarfor the priority queues.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 no solution public Iterablesolution() // sequence of boards in a shortest solution; null if no solution public static void main(String[] args) // solve a slider puzzle (given below) }
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 bythe N-by-Ninitial 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 puzzle-unsolvable3x3.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.
首先我们需要解决一个问题:这个题目是不是一个多项式时间内可以解决的问题?也就是说,这是不是一个P问题呢?答案是否定的,网站上已经给出了答案以及原因,详情如下:
Is there an efficient way to solve the 8-puzzle and its generalizations?Finding a shortest solution to an N-by-N slider puzzle isNP-hard,so it's unlikely that an efficient solution exists.
如果这是一个NP-hard问题,我们只有两种思路:一种是找出概率解,也就是说在多项式时间内做出一个解,这个解有很大的概率是正确的;另一种就是用搜索树等方法来搜索到精确解,但是搜索解本身可能是一个时间复杂度或者空间复杂度为指数复杂度的算法。
Princeton的《Algorithm》给出了一个使用A*搜索的方式来进行解决。其思路和Coursera中另一个公开课《Generic Game Playing》的搜索树算法很像,大致思路是:对于每一个中间解,计算一个这个解是“正确的”大概期望,按照每一个中间解的期望对搜索树进行排序,优先搜索期望大的解。在本题中,所谓的“期望”就是Hamming priority和Manhattan priority。对搜索树排序的方法就是Week4中的优先队列。
上述算法一定能够搜索到正确的解,因为搜索树最极限的情况也就是把所有的可能移动方法都搜索一遍嘛,所以在最糟糕情况下,算法的时间复杂度和空间复杂度都将会是指数级的。
但是,能否搜索到解还有一个情况,就是能否判断8 Puzzle的这个实例是否确实存在解。但是,是否存在解这个确定问题也是一个NP-hard问题,至今没有一个多项式时间算法能在多项式空间复杂度内回答一个8 Puzzle实例是否存在解。针对此问题,《Algorithm》给出了一种判断方法。8 Puzzle问题可分为两类:给定实例存在解,或给定实例的“Twin”实例存在解。因此,整个算法要求构造一个给定实例的Twin实例,然后同时在两个实例中运行搜索算法:如果给定实例找到了解,那么就输出解;如果Twin实例找到了解,那么说明给定实例不存在解,输出null。
算法本身并不复杂,最小优先队列也可以直接使用,我自己实现的算法时间也很快(对于测试的实例,都在2s以内给出搜索解)。但是,我的实现中,空间复杂度比正确答案的空间复杂度大得多!至今我还没有找到这个问题的答案,需要朋友们的帮助了…
我算法的测试分数如下:
我算法的测试结果如下(只有这一部分的Test有部分不通过的情况):
Timing Solver *----------------------------------------------------------- Running 17 total tests. Timing tests use your implementation of Board.java and Solver.java. Maximum allowed time per puzzle is 15 seconds. delMin() filename N seconds insert() + delMax() max PQ size --------------------------------------------------------------------------------------------- => passed puzzle20.txt 3 0.09 4339 2270 2071 => FAILED puzzle21.txt 3 0.15 23546 12303 11245 (1.2x) => passed puzzle22.txt 3 0.04 11521 6024 5499 => passed puzzle23.txt 3 0.10 29234 15271 13965 => passed puzzle24.txt 3 0.08 27638 14519 13121 => passed puzzle25.txt 3 0.07 51689 27040 24651 => FAILED puzzle26.txt 3 0.05 49313 25768 23547 (1.1x) => passed puzzle27.txt 3 0.05 72743 38270 34475 => passed puzzle28.txt 3 0.07 85413 44668 40747 => FAILED puzzle29.txt 3 0.07 96664 (1.2x) 50729 45937 (1.4x) => passed puzzle30.txt 3 0.15 185300 97305 87997 => passed puzzle31.txt 3 0.14 176268 92405 83865 => FAILED puzzle34.txt 4 1.74 1335836 660941 (1.1x) 674897 => passed puzzle37.txt 4 1.01 621008 307365 313645 => passed puzzle39.txt 4 0.58 534206 265519 268689 => passed puzzle41.txt 5 0.53 226458 108667 117793 => passed puzzle44.txt 5 0.65 505048 244981 260069 ==> 13/17 tests passed Total: 13/17 tests passed! ================================================================
根据公开课讨论组中大家的结果,大致的测试结果应该如下:
****************************************************************************** * timing ****************************************************************************** Timing Solver *----------------------------------------------------------- Running 17 total tests. Timing tests use your implementation of Board.java and Solver.java. Maximum allowed time per puzzle is 15 seconds. delMin() filename N seconds insert() + delMax() max PQ size --------------------------------------------------------------------------------------------- => passed puzzle20.txt 3 0.05 1365 815 551 => passed puzzle21.txt 3 0.06 4292 2547 1746 => passed puzzle22.txt 3 0.02 2813 1657 1157 => passed puzzle23.txt 3 0.04 5444 3217 2228 => passed puzzle24.txt 3 0.04 6976 4239 2738 => passed puzzle25.txt 3 0.05 11150 6623 4528 => passed puzzle26.txt 3 0.02 5423 3267 2157 => passed puzzle27.txt 3 0.05 13608 8237 5372 => passed puzzle28.txt 3 0.11 21749 13011 8739 => passed puzzle29.txt 3 0.04 14954 9129 5826 => passed puzzle30.txt 3 0.07 34304 20881 13424 => passed puzzle31.txt 3 0.09 42868 25935 16934 => passed puzzle34.txt 4 0.45 143570 69305 74266 => passed puzzle37.txt 4 1.00 301184 144475 156710 => passed puzzle39.txt 4 1.04 221457 108175 113283 => passed puzzle41.txt 5 0.17 64715 27757 36959 => passed puzzle44.txt 5 0.94 317910 141547 176364 ==> 17/17 tests passed Total: 17/17 tests passed! ================================================================我个人怀疑可能是如下原因里面的几个或者几个:
(1)我的equals方法可能有bug,因此优先队列中增加了很多重复的搜索节点,但是我确实没有发现bug具体在哪里。
(2)我的Board类中,neighbor方法可能有内存管理的bug。一些等价的或者删除的搜索节点并没有被删除。
(3)搜索算法本身有问题。
(4)其他。
我自己已经在讨论组中发了帖子询问了这个问题,帖子如下:
Hi, all:
My answer passed almost all the tests except 4: puzzle21.txt, puzzle 26.txt, puzzle 29.txt, and puzzle 34.txt. The reason why these tests fail is not the time exceeding exceptions, but the memory exceeding exceptions and I have no idea why such happened...
Here are part of the assignment results:
Timing Solver *----------------------------------------------------------- Running 17 total tests. Timing tests use your implementation of Board.java and Solver.java. Maximum allowed time per puzzle is 15 seconds. delMin() filename N seconds insert() + delMax() max PQ size --------------------------------------------------------------------------------------------- => passed puzzle20.txt 3 0.09 4339 2270 2071 => FAILED puzzle21.txt 3 0.15 23546 12303 11245 (1.2x) => passed puzzle22.txt 3 0.04 11521 6024 5499 => passed puzzle23.txt 3 0.10 29234 15271 13965 => passed puzzle24.txt 3 0.08 27638 14519 13121 => passed puzzle25.txt 3 0.07 51689 27040 24651 => FAILED puzzle26.txt 3 0.05 49313 25768 23547 (1.1x) => passed puzzle27.txt 3 0.05 72743 38270 34475 => passed puzzle28.txt 3 0.07 85413 44668 40747 => FAILED puzzle29.txt 3 0.07 96664 (1.2x) 50729 45937 (1.4x) => passed puzzle30.txt 3 0.15 185300 97305 87997 => passed puzzle31.txt 3 0.14 176268 92405 83865 => FAILED puzzle34.txt 4 1.74 1335836 660941 (1.1x) 674897 => passed puzzle37.txt 4 1.01 621008 307365 313645 => passed puzzle39.txt 4 0.58 534206 265519 268689 => passed puzzle41.txt 5 0.53 226458 108667 117793 => passed puzzle44.txt 5 0.65 505048 244981 260069 ==> 13/17 tests passed Total: 13/17 tests passed!
但是至今还没有得到合理的回复… 还请大家帮助了,相信CSDN社区是万能的,嗯嗯~
Board.java
public class Board {
private final int[][] blocks;
private final int N;
// construct a board from an N-by-N array of blocks
// (where blocks[i][j] = block in row i, column j)
public Board(int[][] blocks){
N = blocks.length;
this.blocks = new int[N][];
for (int i=0; i neighbors(){
int blank_i = N;
int blank_j = N;
for (int i=0; i q = new MinPQ(new Comparator() {
public int compare(Board o1, Board o2) {
if (o1.manhattan() < o2.manhattan()) return -1;
else if (o1.manhattan() == o2.manhattan()) return 0;
else return 1;
}
});
if (blank_j - 1 >= 0){
int[][] arr_temp = getCopy();
arr_temp[blank_i][blank_j] = arr_temp[blank_i][blank_j - 1];
arr_temp[blank_i][blank_j - 1] = 0;
q.insert(new Board(arr_temp));
// arr_temp = blocks.clone();
}
if (blank_j + 1 < N){
int[][] arr_temp = getCopy();
arr_temp[blank_i][blank_j] = arr_temp[blank_i][blank_j + 1];
arr_temp[blank_i][blank_j + 1] = 0;
q.insert(new Board(arr_temp));
// arr_temp = blocks.clone();
}
if (blank_i - 1 >= 0){
int[][] arr_temp = getCopy();
arr_temp[blank_i][blank_j] = arr_temp[blank_i - 1][blank_j];
arr_temp[blank_i - 1][blank_j] = 0;
q.insert(new Board(arr_temp));
// arr_temp = blocks.clone();
}
if (blank_i + 1 < N){
int[][] arr_temp = getCopy();
arr_temp[blank_i][blank_j] = arr_temp[blank_i + 1][blank_j];
arr_temp[blank_i + 1][blank_j] = 0;
q.insert(new Board(arr_temp));
// arr_temp = blocks.clone();
}
return q;
}
// string representation of the board (in the output format specified below)
public String toString() {
StringBuilder s = new StringBuilder();
s.append(N + "\n");
for (int i = 0; i < N; i++) {
for (int j = 0; j < N; j++) {
s.append(String.format("%2d ", blocks[i][j]));
}
s.append("\n");
}
return s.toString();
}
private int[][] getCopy(){
int[][] result = new int[N][];
for (int i=0; i
public class Solver {
private boolean isSolve = false;
private int move = -1;
private class SearchNode implements Comparable{
private final Board board;
private final int move;
private final int priority;
private final SearchNode parent;
private final boolean isTwin;
public SearchNode(Board board, int move, SearchNode parent, boolean isTwin){
this.board = board;
this.move = move;
this.priority = board.manhattan() + move;
this.parent = parent;
this.isTwin = isTwin;
}
@Override
public int compareTo(SearchNode that) {
if (this.board.equals(that.board)) return 0;
if (this.priority < that.priority) return -1;
else return 1;
}
}
private MinPQ minPQ = new MinPQ(new Comparator() {
public int compare(SearchNode o1, SearchNode o2) {
if (o1.priority < o2.priority) return -1;
else if (o1.priority == o2.priority) return 0;
else return 1;
}
});
private Stack solutionQueue = new Stack();
// find a solution to the initial board (using the A* algorithm)
public Solver(Board initial){
Board initialTwin = initial.twin();
SearchNode initSearchNode = new SearchNode(initial, 0, null, false);
SearchNode initSearchNodeTwin = new SearchNode(initialTwin, 0, null, true);
minPQ.insert(initSearchNode);
minPQ.insert(initSearchNodeTwin);
solve();
}
private void solve(){
while(true){
//solve for original
SearchNode searchNode = minPQ.delMin();
if (searchNode.board.isGoal()){
if (searchNode.isTwin){
this.isSolve = false;
this.move = -1;
} else {
this.isSolve = true;
this.move = searchNode.move;
this.solutionQueue.push(searchNode.board);
while(searchNode.parent != null){
searchNode = searchNode.parent;
this.solutionQueue.push(searchNode.board);
}
}
break;
}else{
for (Board neiborBoard: searchNode.board.neighbors()){
SearchNode neiborNode = new SearchNode(neiborBoard, searchNode.move+1, searchNode, searchNode.isTwin);
if (searchNode.parent == null){
minPQ.insert(neiborNode);
} else if (!searchNode.parent.board.equals(neiborNode.board)){
minPQ.insert(neiborNode);
}
}
}
}
}
// is the initial board solvable?
public boolean isSolvable(){
return this.isSolve;
}
// min number of moves to solve initial board; -1 if no solution
public int moves(){
return this.move;
}
// sequence of boards in a shortest solution; null if no solution
public Iterable solution(){
if (this.isSolve){
return this.solutionQueue;
}else{
return null;
}
}
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);
}
}
}
public class PuzzleChecker {
public static void main(String[] args) {
String filename = "8puzzle/puzzle44.txt";
// read in the board specified in the filename
In in = new In(filename);
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();
}
}
// solve the slider puzzle
Board initial = new Board(tiles);
Solver solver = new Solver(initial);
System.out.println(filename + ": " + solver.moves());
}
}