Programming Assignment 4: 8 puzzle

8 puzzle

使用A*搜索算法解决8-puzzle问题。

Board.java

Board类用来表示一个 nn n ∗ n 的网格,其中有 n21 n 2 − 1 个方块,每个从1标记到 n21 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,就是给定一个初始的 33 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%]
参考我的github
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值