HDU1043 解题报告

本文介绍了经典八数码问题的三种解决方案:BFS结合康托展开进行打表,双向BFS以及A*算法。详细阐述了每种方法的思路、代码实现和优化点,包括康托展开的概念、有解无解的判断条件以及A*算法中曼哈顿距离的应用。
摘要由CSDN通过智能技术生成

HDU1043:Eight(八数码,经典题型)

原题地址:http://acm.hdu.edu.cn/showproblem.php?pid=1043

题意

​ 这是一道经典的八数码问题,题目给定一个初始状态,要求将这个初始状态转换为目标状态的步骤,目标状态都是一样的,都为1 2 3 4 5 6 7 8 x,注意这道题是Special Judge,即转换的步骤其实是不止一种,而这里只要求输出一种方案即可。

思路

​ 这道题目的解决方案不止一种,事实上有大神总结出了一共有八种方案,详见这里,当然这些方案中有的是铁定TLE的,而有的是小的优化,而我这里主要实现了其中的三种,分别是BFS+Hash+打表,双向BFS+Hash,A*+Hash+曼哈顿距离+优先队列

方案一:BFS+Hash+打表

​ BFS这里就不对其进行赘述了,如果不清楚的请看这里,而对于Hash我想也没必要进行太多的讲解,其实很简单,当我们对所有解空间进行搜索的时候,每搜索到一种新的状态我们都需要对其进行保存,而hash就是用来对每一个搜索到的状态进行一个唯一的编码,例如我们搜索到一个为1 2 3 4 5 6 7 8 x的状态,我们要将其进行保存,以便后续的访问,如果保存在list中的话查找十分麻烦,而如果存在数组中,那我们就需要一种编码方式,对该状态进行唯一的标识,这就是hash函数的作用,而hash函数是有很多种,所以我们就需要一种最为合适的。

​ 在该题中,对于每一个状态,如果我们都将x视为0的话,那么这就是一个从0到8的全排列,既然是全排列的话那就简单了,我们可以直接用该数字作为一个hash编码方式,例如状态为1 2 3 4 5 6 7 8 0就直接保存在数组中该数字对应的位置上,但这就会导致大量的空间浪费,那么我们就需要另一种更优的hash编码方式,我们可以用每一个排列在所有全排列中的位置作为hash编码,例如:0 1 2 3 4 5 6 7 8在所有全排列中的位置是1,那就将该状态存在数组1这个位置上。 然而随之而来的问题是怎么求这个位置,此时我们就需要引入一个概念,即康托展开

康托展开

康拓展开定义:

计算公式:X=an*(n-1)!+an-1*(n-2)!+…+ai*(i-1)!+…+a2*1!+a1*0!

注:X为比该数小的个数,其中ai为当前元素在未出现的元素中是排在第几个(从0开始),n指的是该数的位数

例:比如2143 这个数,求其展开:

从头判断,至尾结束,注意顺序是从左至右。

① 比 2(第一位数)小的数有多少个->1个,换而言之就是2排在第1位,因为第0位排的是1,所以ai就是1,又因为该数一共4位,所以n是4,因此这里ai*(n-1)! -> 1*3!=6

② 比 1(第二位数)小的数有多少个->0个0*2!=0

③ 比 4(第三位数)小的数有多少个->3个就是1,2,3,但是1,2之前已经出现,所以是1*1!=1

将所有乘积相加 6+0+1=7

比该数小的数有7个,所以该数排第8的位置。

1234 1243 1324 1342 1423 1432
2134 2143 2314 2341 2413 2431
3124 3142 3214 3241 3412 3421
4123 4132 4213 4231 4312 4321

康托展开代码实现:

private static int Cantor(int[] a) {
  //计算该状态的康托展开
    int res = 0;
    int fac[] = {
  1, 1, 2, 6, 24, 120, 720, 5040, 40320};//0到9各个数的阶乘
    for (int i = 0; i < a.length; i++) {
        int temp = 0;//比a[i]小的数的个数
        for (int j = i + 1; j < a.length; j++) {
            if (a[i] > a[j]) temp++;//在i之前出现过比a[i]小的数要去掉
        }
        res += temp * fac[a.length - i - 1];//计算
    }
    return res + 1;//返回a在全排列中排第几位
}

​ 到这里,我们其实已经可以对该题进行解答了,我们使用BFS对解答树进行搜索,每搜索到一个新的状态就求其康拓展开,将到达该状态的路径保存到对应的数组中直到搜索到了目标状态1 2 3 4 5 6 7 8 x,但这就会出现一个效率问题,因为该题有N组测试数据,这就导致了我们对每一组测试数据都要重新进行计算,很明显会TLE,这就需要用到一个信息学竞赛的技巧,即打表,所谓打表就是提前对问题所有的解进行求解,并保存在内存中,之后对于每一组测试用例直接返回之前提前解答的结果。

​ 但又有一个问题就是我们并不知道每一次询问的初始状态是什么,那怎么进行打表呢?这也很简单,初始状态我们是不知道,但目标状态呢?这就是我们要进行打表的对象。我们以目标状态作为初始状态进行逆向打表,这样在每一次获得初始状态时就可以直接输出结果。

AC代码:

import java.io.BufferedInputStream;
import java.util.ArrayDeque;
import java.util.Scanner;

/**
 * Created with IntelliJ IDEA.
 *
 * @author wanyu
 * @Date: 2018-01-31
 * @Time: 15:09
 * To change this template use File | Settings | File Templates.
 * @desc 逆向打表+BFS+哈希
 */
public class Main {
   

    private static int[] move = {-1, -3, 1, 3};//移动数组
    private static int[] map;//存储八数码
    private static boolean[] visited;//判断是否已经访问过
    private static String[] path = new String[363000];//保存已经遍历过的状态,使用哈希进行空间压缩

    private static void init(Scanner in) {
  //初始化信息
        String[] line = in.nextLine().split("");//读取输入
        map = new int[9];
        int j = 0;
        for (int i = 0; i < line.length; i++) {
            if (line[i].equals(" ")) continue;
            if (line[i].equals("x")) {
                map[j++] = 0;//对X进行转换
                continue;
            }
            map[j++] = Integer.parseInt(line[i]);
        }
    }

    private static void create() {
  //进行打表操作
        int[] num = {
  1, 2, 3, 4, 5, 6, 7, 8, 0};//以目标状态作为初始状态进行逆向打表
        int cantor = Cantor(num);//计算康拓展开
        path[cantor] = "lr";//初始状态
        visited = new boolean[363000];//用于判重
        Node node = new Node(num, new StringBuffer(""), cantor, 8);//初始节点
        BFS(node);
    }


    public static void main(String[] args) {
        Scanner in = new Scanner(new BufferedInputStream(System.in));
        create();//进行打表
        while (in.hasNext()) {
            init(in);//初始化
            int cantor = Cantor(map);//计算初始状态的康托
            if (path[cantor] == null) {
  //如果没有到该状态的路径表示无解
                System.out.println("unsolvable");
                continue;
            }
            StringBuffer stringBuffer = new StringBuffer(path[cantor]);
            //注意因为是以目标状态作为初始状态进行打表的
            // 所以该路径是从目标状态到初始状态的,所以在输出时需要进行反转
            System.out.println(stringBuffer.reverse());

        }
    }

    private static void BFS(Node start) {
  //使用BFS进行解答树的搜索
        ArrayDeque<Node> queue = new ArrayDeque<>();
        queue.add(start);
        while (!queue.isEmpty()) {
            Node node = queue.poll();//抛出队首元素
            for (int i = 0; i < 4; i++) {
  //对该节点进行扩展
                int index = node.index;
                int new_index = index + move[i];//确定x下一个位置
                //判断当前位置是否可以移动
                if ((index == 2 || index == 5 || index == 8) && i == 2) continue;
                if ((index == 0 || index == 3 || index == 6) && i == 0) continue;
                if (new_index >= 0 && new_index <= 8) {
  //边界处理
                    Node new_node = new Node(node.state, node.path, node.index, node.cantor);//定义新的节点
                    //更新数据
                    new_node.state[index] = new_node.state[new_index];//直接赋值
                    new_node.state[new_index] = 0;
                    new_node.index = new_index;
                    int cantor = Cantor(new_node.state);//计算该状态的康托展开
                    
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值