Princeton Algorithm 8 Puzzle

本文介绍了普林斯顿大学算法课程中关于8 Puzzle问题的解决方案,重点是使用A*算法进行求解。作者强调了在实现过程中如何优化,包括使用优先级队列、缓存关键值以及避免添加重复状态。通过调整优先级队列的策略,如使用Manhattan距离作为平局打破器,代码达到了99分,超过了Coursera上的大多数提交。虽然在个别测试点上仍有优化空间,但整体运行时间远低于5秒限制。作者还分享了个人博客和其他平台链接供读者进一步了解。
摘要由CSDN通过智能技术生成

Princeton Algorithm 8 Puzzle

普林斯顿大学算法课第 4 次作业,8 Puzzle 问题。

这道题目使用了 A* 算法,题目本身就是有点难度的,但是 Specification 里面已经把该算法的步骤都列出来了,基本就是一个优先级队列的使用。而优先级队列也可以使用提供的 MinPQ 完成,所以基本没有难度。

本题的难点依旧在于优化。

Board 的代码还是相对容易的,主要是注意 euqals 必须满足几个特性,并且距离可以做一次缓存。

Solver 我写了一个内部类来用作搜索结点,用结点之间的父亲节点来表示搜索的路径,这样当出现目标局面的时候,逐层沿着 parent 向上就可以得到整条操作路径。

注意 distancepriority 必须缓存,这是一个多达 25 个测试点的优化项目。

另外通过实测可以发现 Manhattan Priority 更加好,所以直接采用这个方案就可以了。

有一个 Breaking tie 的技巧:

  • Using Manhattan() as a tie-breaker helped a lot.

  • Using Manhattan priority, then using Manhattan() to break the tie if two boards tie, and returning 0 if both measurements tie.

Solver 可以在构造的时候直接跑出结果,然后缓存,否则没有执行过 solution() 的话,moves()solvable 也拿不到。

有一个非常关键的地方在于不要添加重复的状态进入 PQ。

node.getParent() == null || !bb.equals(node.getParent().getBoard())

对于判断是不是可解的,可以将 boardboard.twin() 一起加入 PQ,两个状态一起做 A* 搜索,要么是棋盘本身,要么是棋盘的双胞胎,总有一个会做到 isGoal()

一旦有任何一者达到目标局面,就说明这一个情况是可解的,那么另一方就是不可解的。通过判断可解的是自己,还是自己的双胞胎,可以得到 solvable

注意当且仅当 solvable 的时候才会有 moves()solution(),所以对于不可解的状态,注意不要把它的双胞胎的 movessolution 赋值过来。

// To implement the A* algorithm, you must use the MinPQ data type for the priority queue.
MinPQ<GameTreeNode> pq = new MinPQ<>();
// 把当前状态和双胞胎状态一起压入队列,做 A* 搜索
pq.insert(new GameTreeNode(initial, false));
pq.insert(new GameTreeNode(initial.twin(), true));
GameTreeNode node = pq.delMin();
Board b = node.getBoard();
//  要么是棋盘本身,要么是棋盘的双胞胎,总有一个会做到 isGoal()
while (!b.isGoal()) {
   
    for (Board bb : b.neighbors()) {
   
        // The critical optimization.
        // A* 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 in the game tree.
        if (node.getParent() == null || !bb.equals(node.getParent().getBoard())) {
   
            pq.insert(new GameTreeNode(bb, node));
        }
    }
    // 理论上这里 pq 永远不可能为空
    node = pq.delMin();
    b = node.getBoard();
}
// 如果是自己做出了结果,那么就是可解的,如果是双胞胎做出了结果,那么就是不可解的
solvable = !node.isTwin();
 
if (!solvable) {
   
    // 注意不可解的地图,moves 是 -1,solution 是 null
    moves = -1;
    solution = null;
} else {
   
    // 遍历,沿着 parent 走上去
    ArrayList<Board> list = new ArrayList<>();
    while (node != null) {
   
        list.add(node.getBoard());
        node = node.getParent();
    }
    // 有多少个状态,减 1 就是操作次数
    moves = list.size() - 1;
    // 做一次反转
    Collections.reverse(list);
    solution = list;
}

这段代码得了 99 分,应该已经秒杀了 Coursera 上绝大多数的提交了。

这次 Assignment 的及格线是 80 分,应该说只要正确性达标,内存和时间做的差些,90 分还是可以有的。

主要可能还是有些细节的地方没有优化到,MinPQ Operation CountBoard Operation Count 这两个测试有部分测试数据没过,应该是哪里还能省掉几次调用。但是在整体的运行时间上,只有 2 个测试数据超过了 1 秒,分别为 1.25 秒和 1.29 秒,其余测试点均在 0.X 秒就完成了,远小于测试规定的 5 秒以内。

Compilation:  PASSED
API:          PASSED
 
Spotbugs:     PASSED
PMD:          PASSED
Checkstyle:   PASSED
 
Correctness:  51/51 tests passed
Memory:       22/22 tests passed
Timing:       116/125 tests passed

以下代码获得 99 分

import java.util.ArrayList;
import java.util.Arrays;
 
public class Board {
   
 
    private final int[][] tiles;
    private final int n;
    // 缓存每一个位置的距离,需要的时候可以不用每次都重新遍历计算
    private final int hamming;
    private final int manhattan;
 
    // create a board from an n-by-n array of tiles,
    // where tiles[row][col] = tile at (row, col)
    public Board(int[][] tiles) {
   
        n = tiles.length;
        this.tiles = new int[n][n];
        int hammingSum = 0;
        int manhattanSum = 0;
        // 复制值,而不是令 this.tiles = tiles,确保 Immutable
        for (int i = 0; i < n; i++) {
   
            for (int j = 0; j < n; j++) 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

凝神长老

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值