「CS61B」Note(5) HW4 Puzzle Solver

In this lab, the task is building an artificial intelligence that solves puzzles. The requirements are at this link.

Puzzle Solver

First, define a search node of the puzzle to be:

private class SearchNode{
    WorldState worldState;
    int distanceFromInit;
    SearchNode previousNode;
    //second optimization : save estimatedDistanceToGoal of WorldState in an instance variable
    int estimatedDistanceToGoalSN;

    public SearchNode(WorldState ws, int dfi, SearchNode psn) {
        worldState = ws;
        distanceFromInit = dfi;
        previousNode = psn;
        estimatedDistanceToGoalSN = ws.estimatedDistanceToGoal();
    }
}

In this lab, we need to use MinPQ class from edu.princeton.cs.algs4 for the priority queue and implement priority sorting. Because the constructor function MinPQ(Comparator<Key> comparator)takes a comparator to compare keys in the PQ, we need to write a NodeComparator :

private class NodeComparator implements Comparator<SearchNode> {
    @Override
    public int compare(SearchNode sn1, SearchNode sn2) {
        return sn1.distanceFromInit + sn1.estimatedDistanceToGoalSN
                - (sn2.distanceFromInit + sn2.estimatedDistanceToGoalSN);
    }
}

Then, just instantiate the NodeComparator and put it as the input of the constructor of MinPQ:

private MinPQ<SearchNode> pq = new MinPQ<>(new NodeComparator());

Constructor which solves the puzzle, computing everything necessary for moves() and solution() to not have to solve the problem again. Solves the puzzle using the A* algorithm.

And there is 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 world state is the same as the world state of the previous search node. So check if the WorldState is its own grandparent everytime we insert it into pq.

private MinPQ<SearchNode> pq = new MinPQ<>(new NodeComparator());
private int moves;
private SearchNode minSearchNode;

/** Constructor which solves the puzzle, computing
 * everything necessary for moves() and solution() to
 * not have to solve the problem again. Solves the
 * puzzle using the A* algorithm. Assumes a solution exists. */
public Solver(WorldState initial) {
    //insert an “initial search node” into the priority queue
    SearchNode initialNode = new SearchNode(initial, 0, null);
    pq.insert(initialNode);
    //Remove the search node with minimum priority
    minSearchNode = pq.delMin();
    while (!minSearchNode.worldState.isGoal()) {
        for (WorldState ws : minSearchNode.worldState.neighbors()) {
            //critical optimization : checks that no enqueued WorldState is its own grandparent
            if (minSearchNode.previousNode == null || !ws.equals(minSearchNode.previousNode.worldState)) {
                pq.insert(new SearchNode(ws,minSearchNode.distanceFromInit + 1, minSearchNode));
            }
        }
        minSearchNode = pq.delMin();
    }
    moves = minSearchNode.distanceFromInit;
}

move() and solution(): For the solution(), it returns a collection of WorldStates of the solution that is iterable. We need to use previousNode of SearchNode to get all of the WorldStates, so the oder will be reversed. Using a first-in last-out stack is appropriate for this task.

/** Returns the minimum number of moves to solve the puzzle starting
 * at the initial WorldState. */
int moves() { return moves; }

/** Returns a sequence of WorldStates from the initial WorldState
 * to the solution. */
public Iterable<WorldState> solution() {
    Stack<WorldState> stack = new Stack<>();
    while (minSearchNode != null) {
        stack.push(minSearchNode.worldState);
        minSearchNode = minSearchNode.previousNode;
    }
    return stack;
}

But the solution()failed the Test Immutability of Solver on Autograder, the reason is in the code above, I changed minSearchNode when solution() runs. So the Iterable will be different everytime. The correct version is as follow:

public Iterable<WorldState> solution() {
    Stack<WorldState> stack = new Stack<>();
    SearchNode thisSearchNode = minSearchNode;
    while (thisSearchNode != null) {
        stack.push(thisSearchNode.worldState);
        thisSearchNode = thisSearchNode.previousNode;
    }
    return stack;
}

Board

In the Board constructor, if you just copy the reference, someone can change the state of your Board by changing the array. For example, if we use this.tiles = tiles in the Board constructor and change the values of tiles from the outside, then the tiles of the Board is also gonna be changed. This is called swallow copy, which only pass the reference(or address) to the new object. So instead, we need to use deep copy to avoid this.

public class Board implements WorldState{
    private int[][] tiles;

    public Board(int[][] tiles) {
        //implement deep copy of input tiles
        this.tiles = new int[tiles.length][tiles.length];
        for (int i = 0; i < tiles.length; i++) {
            System.arraycopy(tiles[i], 0, this.tiles[i], 0, tiles.length);
        }
    }
    public int tileAt(int i, int j) {
        if (i > size() - 1 || i < 0 || j > size() - 1 || j < 0) {
            throw new java.lang.IndexOutOfBoundsException();
        }
        return tiles[i][j];
    }

    public int size() {
        return tiles.length;
    }

For the neighbors()function, I just used the solution provided by the teacher.

Goal Distance Estimates:

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
public int hamming() {
    int hamming = 0;
    for (int i = 0; i < size(); i++) {
        for (int j = 0; j < size(); j++) {
            if (tiles[i][j] != 0 && tiles[i][j] != i * size() + j + 1) {
                hamming ++;
            }
        }
    }
    return hamming;
}

public int manhattan() {
    int manhattan = 0;
    for (int i = 0; i < size(); i++) {
        for (int j = 0; j < size(); j++) {
            if (tiles[i][j] != 0) {
                manhattan += Math.abs((tiles[i][j] - 1) / size() - i) + Math.abs((tiles[i][j] - 1) % size() - j);
            }
        }
    }
    return manhattan;
}
@Override
public int estimatedDistanceToGoal() { return manhattan(); }

public boolean equals(Object y) {
    return Arrays.deepEquals(this.tiles, ((Board) y).tiles);
}

equals()failed the test. The reason is I didn’t consider the situation that y isn’t a Board and is-self. Arrays.deepEquals(Object[] o1,Object[] o2)checks if each value of two multidimensional arrays is equal.

public boolean equals(Object y) {
    if (y == this) return true;
    if (y == null || y.getClass() != this.getClass()) return false;
    return Arrays.deepEquals(this.tiles, ((Board) y).tiles);
}