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);//计算该状态的康托展开