题目传送门
Input
You will receive, several descriptions of configuration of the 8 puzzle. One description is just a list of the tiles in their initial positions, with the rows listed from top to bottom, and the tiles listed from left to right within a row, where the tiles are represented by numbers 1 to 8, plus ‘x’. For example, this puzzle
1 2 3
x 4 6
7 5 8
is described by this list:
1 2 3 x 4 6 7 5 8
Output
You will print to standard output either the word ``unsolvable’’, if the puzzle has no solution, or a string consisting entirely of the letters ‘r’, ‘l’, ‘u’ and ‘d’ that describes a series of moves that produce a solution. The string should include no spaces and start at the beginning of the line. Do not print a blank line between cases.
Sample Input
2 3 4 1 5 x 7 6 8
Sample Output
ullddrurdllurdruldr
思路
八数码问题是搜索进阶必刷题,也是非常经典的好题,难点主要有两点:
-
判重问题(MLE),这个判重还是挺啰嗦的一个东西。由于展开是9的阶乘大概37W种情况,每一种情况判重用如果用set判int,sizeof(int) * 370000要这么多字节。很显然内存是不够的,这道题判重要么康托展开判重要么set对string判重。因为每一种排列情况都是9!以内,所以可以对排列得到的数字组合大小在9个数字中排列属于第几大。
-
超时问题,TLE是因为搜索情况太多了,每次都需要重复搜索,最坏的情况下每一次都是搜完37w种情况。数据大肯定过不去,由于这道题最终答案都是12345678x,所以从最终答案出发离线打表并且反向记录答案(从目标结果出发)所有的情况并且记录下来,之后输入就是直接找到对应的数值输出即可。
本题解法不唯一bfs做法是离线。
//逆向bfs打表,目标终点状态一样从目标开始打表出不到40W种的所有情况
//从起点状态str1回溯目标状态。
#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
#include <queue>
using namespace std;
int sum[10] = {1,1,2,6,24,120,720,5040,40320,362880};
int dx[] = {0,1,-1,0};
int dy[] = {-1,0,0,1};
char dir[5] = {"dlru"};
struct info{
char way; //记录方法
int pre; //记录前驱
}w[370000];
struct Node{
int ct; //康托值
int state[9]; //状态
int num; //9的位置
};
queue<Node>q;
int Cantor(int s[],int n) //康托展开判重
{
int result = 0;
for(int i = 0;i < n;i++){
int cnt = 0;
for(int j = i+1;j < n;j++){
if(s[i] > s[j]){
cnt++;
}
}
result += cnt * sum[n-i-1];
}
return result + 1; //一定要+1,有些不+1可能会对,WA两页才找到的错误。
}
void bfs()
{
while(!q.empty()){
Node ptr = q.front(),p;
q.pop();
for(int i = 0;i < 4;i++){
int nx = ptr.num % 3 + dx[i]; //转化为二维图数字9的交换后横坐标
int ny = ptr.num / 3 + dy[i]; //转化为二维图数字9的交换后纵坐标
int nz = nx + 3*ny; //算出新的一维为坐标
if(nx < 0 || nx >= 3 || ny < 0 || ny >= 3){ //边界检查
continue;
}
memcpy(&p,&ptr,sizeof(struct Node)); //结构体赋值
p.num = nz; //9的新位置
swap(p.state[nz],p.state[ptr.num]); //新老位置交换
p.ct = Cantor(p.state,9); //计算康托值
if(w[p.ct].pre == -1){ //判断该康托值是否之前就拓展过,先拓展的一定是最小的
w[p.ct].pre = ptr.ct; //连接当前状态的上一个状态
w[p.ct].way = dir[i]; //这里记录需要搞反向
q.push(p);
}
}
}
}
void slove()
{
while(!q.empty()){
q.pop();
}
int a[9] = {1,2,3,4,5,6,7,8,9};
Node p;
for(int i = 0;i < 370000;i++){
w[i].pre = -1;
}
memcpy(p.state,a,sizeof(p.state)); //状态图复制
p.ct = 0; //初始康托值为0
w[p.ct].pre = 0; //前驱为0,此时这是树根位置
p.num = 8; //x的初始位置
q.push(p);
bfs(); //预处理所有的状态
}
int main()
{
slove(); //预处理
char s[100];
while(gets(s)){
int len = strlen(s);
int t[9],j = 0;
for(int i = 0;i < len;i++){
if(s[i] >= '1' && s[i] <= '8'){
t[j++] = s[i] - '0';
}
else if(s[i] == 'x'){
t[j++] = 9;
}
}
int ans = Cantor(t,9);
if(w[ans].pre == -1){
printf("unsolvable\n");
}
else{
while(ans){
printf("%c",w[ans].way); //打印方法
ans = w[ans].pre; //向前回溯
}
printf("\n");
}
}
return 0;
}
//以目标节点为中心,拓展出一棵树,每个节点都是一种状态。
A* 或者 IDA* + 康托展开 + 无解情况剪枝也能做,只不过这两个解法是在线强行算。具体这个无解情况剪枝认真研究也要研究一天,感觉上面离线打表的做法比较好。
八数码问题A*搜索的h函数
第一种,每个数字到对应位置需要移动的格子数目
第二种,有多少个在对应位置的个数
其实Astar搜索无非就是bfs搜索的拓展,只不过加入了估价函数,对于这个估价函数呢你也可以加入自己的思路去把它设计的更完美,让搜索跑的更快。当然Astar搜索对于普通bfs搜索的优点就是在有解的情况下更快,如果题目存在无解的情况那基本上没啥大的提升,反正这两种搜索都会搜索完所有的可能情况,只不过先后顺序可能不一样罢了,如果题目没有无解的情况或者你能够把无解的情况单独拿出来剪枝那么使用Astar能够加快搜索,如果无解的情况你剪枝不掉或者剪枝掉意义不大那么普通广搜和Astar理论上差不多。
下面代码采用的是第一种估价计算函数。
#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
#include <queue>
#include <cmath>
using namespace std;
const int maxn = 370000;
int sum[10] = {1,1,2,6,24,120,720,5040,40320,362880};
bool visited[maxn];
int ed[] = {1,2,3,4,5,6,7,8,9};
int dx[] = {0,-1,1,0};
int dy[] = {1,0,0,-1};
char dir[5] = {"dlru"};
struct info{
int state[10];
int g,f,h,num;
char way[50];
bool operator <(const info s)const{
if(s.f == f){
return s.g < g;
}
else{
return s.f < f;
}
}
};
int Cantor(int s[]) //康托展开
{
int result = 0;
for(int i = 0;i < 9;i++){
int cnt = 0;
for(int j = i+1;j < 9;j++){
if(s[i] > s[j]){
cnt++;
}
}
result += cnt*sum[8-i];
}
return result + 1;
}
int Manhaton(int st[]) //曼哈顿估计,每一个数字归位需要多少步
{
int result = 0;
for(int i = 0;i < 9;i++){
int num = st[i] - 1;
int x = i/3;
int y = i%3;
int a = num/3;
int b = num%3;
result += abs(x-a) + abs(y-b);
}
return result;
}
priority_queue<info>q;
int Astar()
{
while(!q.empty()){
info ptr = q.top(),p;
q.pop();
for(int i = 0;i < 4;i++){
int nx = ptr.num%3 + dx[i];
int ny = ptr.num/3 + dy[i];
int nz = nx + 3*ny;
if(nx < 0 || nx >= 3 || ny < 0 || ny >= 3){
continue;
}
memcpy(&p,&ptr,sizeof(struct info));
swap(p.state[ptr.num],p.state[nz]);
p.num = nz;
int ct = Cantor(p.state);
if(visited[ct]){ //判重
continue;
}
p.way[ptr.g] = dir[i];
p.g = ptr.g + 1;
p.h = Manhaton(p.state);
p.f = p.g + p.h;
if(p.h == 0){ //目标状态
p.way[p.g] = '\0';
printf("%s\n",p.way);
return p.g;
}
q.push(p);
visited[ct] = true;
}
}
return -1;
}
int main()
{
char s[100];
while(gets(s)){
int len = strlen(s);
int t[10],j = 0,p_x = 0;
for(int i = 0;i < len;i++){
if(s[i] == 'x'){
p_x = j;
t[j++] = 9;
continue;
}
else if(s[i] >= '1' && s[i] <= '8'){
t[j++] = s[i] - '0';
}
}
int sum = 0;
for(int i = 0;i < j;i++){
if(t[i] == 9){
continue;
}
for(int k = 0;k < i;k++){
if(t[k] == 9){
continue;
}
if(t[i] < t[k]){
sum++;
}
}
}
memset(visited,false,sizeof(visited));
while(!q.empty()){
q.pop();
}
info p;
memcpy(p.state,t,sizeof(t));
p.g = 0;p.h = Manhaton(t);
if(p.h == 0){
printf("\n");
continue;
}
if(sum % 2 == 1){ //剪枝掉无解的情况,不然还是会超时。
printf("unsolvable\n");
continue;
}
p.f = p.h + p.g;p.num = p_x;
p_x = Cantor(p.state);
visited[p_x] = true;
q.push(p);
Astar();
}
return 0;
}
愿你走出半生,归来仍是少年~