題目傳送門
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;
}
願你走出半生,歸來仍是少年~