经典的八数码问题,花了一天时间浏览了很多博客,看了大佬们各种各样的解题方法,写出这一篇博客作为相关问题解法的总结,尽量写得详细,也会把看到的比较好的讲解的链接放在文章里面,如果有兴趣可以点开来看一看,希望能对理解有所帮助。
HDU - 1043 Eight
首先从题目中提取信息,输入多组初状态,已知单一末状态,求每组初状态到达末状态经过的路径,且初状态不一定能到达末状态。如果从每个初状态开搜,在最坏的情况下,每组初状态都要把它的所有可以转移到的状态都到达一遍。此时,在多对一的情况下,从末状态开搜会更有效,只需要把唯一一个末状态的所有可以转移到的状态都访问一遍,做标记并记录下从末状态到当前状态的路径,输入多组数据时只需要通过标记判断该初状态是否能由末状态转移得到,如果可以直接反过来打表,非常的方便。其中,为了用数组存储和表示序列的路径与标记,使用到了一种状态压缩技巧:
算法基础:康托展开与逆康托展开
将序列压缩成Hash后,就可以用Hash做数组的下标,简化了转移过程。
#include //HDU提交请换成其他头文件
using namespace std;
const int MAXN=362885;//序列9876543210的Cantor为362880,下标不会超过这个值
const int fac[]= {1,1,2,6,24,120,720,5040,40320,362880};
int dx[]= {0,0,-1,1};
int dy[]= {-1,1,0,0};
char sign[]= {'d','u','r','l'};//因为是反着输出,所以方向是相反的
int state[]= {1,2,3,4,5,6,7,8,0};//搜索从末状态开始
bool vis[MAXN];
string path[MAXN];
struct node
{
int n,cur;//n是hash值,cur是x在九宫格的位置(从0开始)
} st,temp;
int cantor(int *a)//康托展开
{
int sum=0;
for (int i=0; i<9; i++)
{
int cnt=0;
for(int j=i+1; j<9; j++)
if(a[j]<a[i])
cnt++;
sum+=cnt*fac[9-i-1];
}
return sum+1;//算出的是按照字典序,排在当前序列之前的序列的个数,加上1后才得到当前序列的排位
}
void recantor(int *a,int n)//逆康托展开
{
n-=1;//同理,将当前序列排位-1后得到前置序列个数,再加入计算
vector<int> v;
for(int i=0; i<9; i++)
v.push_back(i);
for(int i=0; i<9; i++)
{
int place=n/fac[9-i-1];
n%=fac[9-i-1];
sort(v.begin(),v.end());
a[i]=v[place];
v.erase(v.begin()+place);
}
}
void bfs()
{
memset(vis,false,sizeof(vis));
queue<node> que;
st.n=cantor(state);
st.cur=8;
path[st.n]="";
que.push(st);
vis[st.n]=true;
while(!que.empty())
{
st=que.front();
que.pop();
for(int i=0; i<4; i++)
{
int xx=st.cur%3+dx[i];
int yy=st.cur/3+dy[i];
if(xx>=0&&xx<=2&&yy>=0&&yy<=2)
{
temp=st;
temp.cur=yy*3+xx;
recantor(state,temp.n);
swap(state[temp.cur],state[st.cur]);
temp.n=cantor(state);
if(!vis[temp.n])
{
path[temp.n]=sign[i]+path[st.n];//因为是倒着BFS,所以路径也是从后往前倒着接
que.push(temp);
vis[temp.n]=true;
}
}
}
}
}
int main()
{
bfs();
char ch;
while(cin>>ch)
{
if(ch=='x')
state[0]=0;
else
state[0]=ch-'0';
for(int i=1; i<9; i++)
{
cin>>ch;
if(ch=='x')
state[i]=0;
else
state[i]=ch-'0';
}
int has=cantor(state);
if(vis[has])
cout<<path[has]<<endl;
else
cout<<"unsolvable"<<endl;
}
}
Time 936ms
Mem 18MB
真的只能逆向BFS解决这道题吗?回顾刚才说的,如果对每组初状态都开搜,最坏的情况需要把每组初状态所能到达的所有状态全部访问一遍,即所有初节点都无法通过转移到达末节点,只能一直BFS到队列为空。其实有一种非常巧妙的方法可以在一开始就判断出该初状态能否到达末状态:
再论八数码
最近正好在学A *,评论区看到有人给出了A *的的题解,抱着好奇的心态去搜了一下,发现了这篇文章。有了文中的结论,可以在最开始就把不可能到达的状态进行剪枝,使得正向BFS解决这道题成为了可能。
而A *部分和往常一样,采用曼哈顿距离公式作为乐观估价函数,BFS时使用优先队列。
#include
using namespace std;
const int MAXN=362885;
const int fin=46234;//末状态的Hash值,用于判断是否到达终点
const int fac[]= {1,1,2,6,24,120,720,5040,40320,362880};
int dx[]= {0,0,-1,1};
int dy[]= {-1,1,0,0};
char sign[]= {'u','d','l','r'};
int state[10];
bool vis[MAXN];
string path[MAXN];
struct node
{
int n,cur,f,g,h;//A*中的f,g,h
node() {}
bool operator <(const node &a)const
{
if(a.f!=f)
return a.f<f;
return a.g<g;
}
} st,temp;
int guess()//曼哈顿距离函数
{
int res=0;
for(int i=0; i<9; i++)
{
int x=i%3,y=i/3;
if(state[i])
{
int xx=(state[i]-1)%3,yy=(state[i]-1)/3;
res+=abs(x-xx)+abs(y-yy);
}
}
return res;
}
int cantor(int *a)
{
int sum=0;
for (int i=0; i<9; i++)
{
int cnt=0;
for(int j=i+1; j<9; j++)
if(a[j]<a[i])
cnt++;
sum+=cnt*fac[9-i-1];
}
return sum+1;
}
void recantor(int *a,int n)
{
n-=1;
vector<int> v;
for(int i=0; i<9; i++)
v.push_back(i);
for(int i=0; i<9; i++)
{
int place=n/fac[9-i-1];
n%=fac[9-i-1];
sort(v.begin(),v.end());
a[i]=v[place];
v.erase(v.begin()+place);
}
}
void bfs()
{
memset(vis,0,sizeof(vis));
st.n=cantor(state);
path[st.n]="";
st.g=0;
st.h=guess();
st.f=st.g+st.h;
priority_queue<node> que;
que.push(st);
vis[st.n]=true;
while(!que.empty())
{
st=que.top();
que.pop();
if(st.n==fin)
{
cout<<path[st.n]<<endl;
return;
}
for(int i=0; i<4; i++)
{
int xx=st.cur%3+dx[i],yy=st.cur/3+dy[i];
if(xx>=0&&xx<=2&&yy>=0&&yy<=2)
{
temp.cur=yy*3+xx;
recantor(state,st.n);
swap(state[st.cur],state[temp.cur]);
temp.n=cantor(state);
if(!vis[temp.n])
{
path[temp.n]=path[st.n]+sign[i];
temp.g=st.g+1;
temp.h=guess();
temp.f=temp.g+temp.h;
que.push(temp);
vis[temp.n]=true;
}
}
}
}
}
int main()
{
char ch;
while(cin>>ch)
{
if(ch=='x')
{
state[0]=0;
st.cur=0;
}
else
state[0]=ch-'0';
for(int i=1; i<9; i++)
{
cin>>ch;
if(ch=='x')
{
state[i]=0;
st.cur=i;
}
else
state[i]=ch-'0';
}
int cnt=0;
for(int i=0; i<8; i++)
for(int j=i+1; j<9; j++)
if(state[j]<state[i]&&state[i]&&state[j])
cnt++;//逆序对计数
if(cnt&1)//按照结论,逆序对个数为奇数则无解
cout<<"unsolvable"<<endl;
else
bfs();
}
}
Time 2745ms
Mem 28.1MB
虽然通过了这道题,但是这种方法相对于反向BFS要消耗3倍的时间和1.6倍的空间。在翻阅其他博客时,发现了一种优化方法,即在状态向后转移时建立一个向前的单向链表,当有分支到达终点时,只要通过递归返回起点,再把路径依次正向输出即可,这样的路径存储方式节省了大量的空间,但是对时间的影响不大。
#include
using namespace std;
const int MAXN=362885;
const int fin=46234;
const int fac[]= {1,1,2,6,24,120,720,5040,40320,362880};
int dx[]= {0,0,-1,1};
int dy[]= {-1,1,0,0};
char sign[]= {'u','d','l','r'};
int endstate[]= {1,2,3,4,5,6,7,8,0};
int state[10];
bool vis[MAXN];
char path[MAXN];//path[n]存储上一个状态到达此状态所移动的方向
int fr[MAXN];//fr[n]存储上一个状态的Hash
struct node
{
int n,cur,f,g,h;
node() {}
bool operator <(const node &a)const
{
if(a.f!=f)
return a.f<f;
return a.g<g;
}
} st,temp;
int guess()
{
int res=0;
for(int i=0; i<9; i++)
{
int x=i%3,y=i/3;
if(state[i])
{
int xx=(state[i]-1)%3,yy=(state[i]-1)/3;
res+=abs(x-xx)+abs(y-yy);
}
}
return res;
}
int cantor(int *a)
{
int sum=0;
for (int i=0; i<9; i++)
{
int cnt=0;
for(int j=i+1; j<9; j++)
if(a[j]<a[i])
cnt++;
sum+=cnt*fac[9-i-1];
}
return sum+1;
}
void recantor(int *a,int n)
{
n-=1;
vector<int> v;
for(int i=0; i<9; i++)
v.push_back(i);
for(int i=0; i<9; i++)
{
int place=n/fac[9-i-1];
n%=fac[9-i-1];
sort(v.begin(),v.end());
a[i]=v[place];
v.erase(v.begin()+place);
}
}
void print(int n)//递归输出
{
if(fr[n])
{
print(fr[n]);
cout<<path[n];
}
}
void bfs()
{
memset(vis,0,sizeof(vis));
memset(fr,0,sizeof(fr));
st.n=cantor(state);
st.g=0;
st.h=guess();
st.f=st.g+st.h;
priority_queue<node> que;
que.push(st);
vis[st.n]=true;
while(!que.empty())
{
st=que.top();
que.pop();
if(st.n==fin)
{
print(fin);
cout<<endl;
return;
}
for(int i=0; i<4; i++)
{
int xx=st.cur%3+dx[i],yy=st.cur/3+dy[i];
if(xx>=0&&xx<=2&&yy>=0&&yy<=2)
{
temp.cur=yy*3+xx;
recantor(state,st.n);
swap(state[st.cur],state[temp.cur]);
temp.n=cantor(state);
if(!vis[temp.n])
{
path[temp.n]=sign[i];
fr[temp.n]=st.n;
temp.g=st.g+1;
temp.h=guess();
temp.f=temp.g+temp.h;
que.push(temp);
vis[temp.n]=true;
}
}
}
}
}
int main()
{
char ch;
while(cin>>ch)
{
if(ch=='x')
{
state[0]=0;
st.cur=0;
}
else
state[0]=ch-'0';
for(int i=1; i<9; i++)
{
cin>>ch;
if(ch=='x')
{
state[i]=0;
st.cur=i;
}
else
state[i]=ch-'0';
}
int cnt=0;
for(int i=0; i<8; i++)
for(int j=i+1; j<9; j++)
if(state[j]<state[i]&&state[i]&&state[j])
cnt++;
if(cnt&1)
cout<<"unsolvable"<<endl;
else
bfs();
}
}
Time 2418ms
Mem 3.8MB
在Virtual Judge评论区看见了有使用IDA*解这道题的方法,顺便学习了一下,同样是使用曼哈顿距离作为估价函数,效率非常高。
#include
#include
#include
#include
#include
using namespace std;
int dx[]={1,0,0,-1};
int dy[]={0,-1,1,0};
char sign[]= {'r','u','d','l'};
int state[10];
char path[1000];
int maxdeep;
bool flag;
int guess()
{
int res=0;
for(int i=0; i<9; i++)
{
int x=i%3,y=i/3;
if(state[i])
{
int xx=(state[i]-1)%3,yy=(state[i]-1)/3;
res+=abs(x-xx)+abs(y-yy);
}
}
return res;
}
void dfs(int x,int y,int pre,int deep)
{
if(flag)
return;
int dis=guess();
if(!dis)
{
cout<<path<<endl;
flag=1;
return;
}
if(dis+deep>maxdeep)
return;
for(int i=0;i<4&&!flag;i++)
{
if(i+pre==3) continue;
int xx=x+dx[i],yy=y+dy[i];
if(xx>=0&&xx<=2&&yy>=0&&yy<=2)
{
path[deep]=sign[i];
swap(state[3*y+x],state[3*yy+xx]);
dfs(xx,yy,i,deep+1);
swap(state[3*y+x],state[3*yy+xx]);
}
}
}
int main()
{
int x,y;
char ch;
while(cin>>ch)
{
memset(path,0,sizeof(path));
flag=0;
if(ch=='x')
{
state[0]=0;
x=0,y=0;
}
else
state[0]=ch-'0';
for(int i=1; i<9; i++)
{
cin>>ch;
if(ch=='x')
{
state[i]=0;
x=i%3,y=i/3;
}
else
state[i]=ch-'0';
}
int cnt=0;
for(int i=0; i<8; i++)
for(int j=i+1; j<9; j++)
if(state[j]<state[i]&&state[i]&&state[j])
cnt++;
if(cnt&1)
cout<<"unsolvable"<<endl;
else
for(maxdeep=1;maxdeep<=1000000&&!flag;maxdeep++)
dfs(x,y,-1,0);
}
}
Time 421ms
Mem 1776kB
双向BFS还是使用原来的字符串数组记录路径,否则需要两组记录路径的数组和两个方向的递归函数,十分麻烦。
#include
using namespace std;
const int MAXN=362885;
const int fac[]= {1,1,2,6,24,120,720,5040,40320,362880};
int dx[]= {0,0,-1,1};
int dy[]= {-1,1,0,0};
char sign1[]= {'u','d','l','r'};
char sign2[]= {'d','u','r','l'};
int endstate[]= {1,2,3,4,5,6,7,8,0};
int state[10];
bool vis1[MAXN],vis2[MAXN];
string path[MAXN];
struct node
{
int n,cur,deep;
} st,temp;
int cantor(int *a)
{
int sum=0;
for (int i=0; i<9; i++)
{
int cnt=0;
for(int j=i+1; j<9; j++)
if(a[j]<a[i])
cnt++;
sum+=cnt*fac[9-i-1];
}
return sum+1;
}
void recantor(int *a,int n)
{
n-=1;
vector<int> v;
for(int i=0; i<9; i++)
v.push_back(i);
for(int i=0; i<9; i++)
{
int place=n/fac[9-i-1];
n%=fac[9-i-1];
sort(v.begin(),v.end());
a[i]=v[place];
v.erase(v.begin()+place);
}
}
void bfs()
{
memset(vis1,0,sizeof(vis1));
memset(vis2,0,sizeof(vis2));
queue<node> que1,que2;
st.n=cantor(state);
st.deep=0;
path[st.n]="";
que1.push(st);
vis1[st.n]=true;
st.n=cantor(endstate);
st.deep=0;
st.cur=8;
path[st.n]="";
que2.push(st);
vis2[st.n]=true;
int L=0;
while(!que1.empty()||!que2.empty())
{
while(que1.front().deep==L)
{
st=que1.front();
que1.pop();
int x=st.cur%3,y=st.cur/3;
for(int i=0; i<4; i++)
{
int xx=x+dx[i],yy=y+dy[i];
if(xx>=0&&xx<=2&&yy>=0&&yy<=2)
{
temp.cur=yy*3+xx;
recantor(state,st.n);
swap(state[st.cur],state[temp.cur]);
temp.n=cantor(state);
if(vis2[temp.n])
{
cout<<path[st.n]<<sign1[i]<<path[temp.n]<<endl;
return;
}
if(!vis1[temp.n])
{
temp.deep=st.deep+1;
path[temp.n]=path[st.n]+sign1[i];
que1.push(temp);
vis1[temp.n]=true;
}
}
}
}
while(que2.front().deep==L)
{
st=que2.front();
que2.pop();
int x=st.cur%3,y=st.cur/3;
for(int i=0; i<4; i++)
{
int xx=x+dx[i],yy=y+dy[i];
if(xx>=0&&xx<=2&&yy>=0&&yy<=2)
{
temp.cur=3*yy+xx;
recantor(state,st.n);
swap(state[st.cur],state[temp.cur]);
temp.n=cantor(state);
if(vis1[temp.n])
{
cout<<path[temp.n]<<sign2[i]<<path[st.n]<<endl;
return;
}
if(!vis2[temp.n])
{
temp.deep=st.deep+1;
path[temp.n]=sign2[i]+path[st.n];
que2.push(temp);
vis2[temp.n]=true;
}
}
}
}
L++;
}
}
int main()
{
char ch;
while(cin>>ch)
{
if(ch=='x')
{
state[0]=0;
st.cur=0;
}
else
state[0]=ch-'0';
for(int i=1; i<9; i++)
{
cin>>ch;
if(ch=='x')
{
state[i]=0;
st.cur=i;
}
else
state[i]=ch-'0';
}
int cnt=0;
for(int i=0; i<8; i++)
for(int j=i+1; j<9; j++)
if(state[j]<state[i]&&state[i]&&state[j])
cnt++;
if(cnt&1)
cout<<"unsolvable"<<endl;
else
bfs();
}
}
Time 2527ms
Mem 26.7MB