题目:http://acm.hdu.edu.cn/showproblem.php?pid=1043
说实话,这道题我算是写了很久了,这道题当然也让我收获甚多,作为新手,刚开始的话,我是用 map
当然,打表的方法不用判断所谓的有无解的问题,因为,我们已经枚举了出了所有可行的起点,只要是能到达终点状态的起点,其vis在枚举时已经变成了 1。
看到,有人说:八数码问题决定了一生是否完美的。这足以说明,此题的重要性。
所以我有决定用其他代码来解决此问题:A* 和 双 BFS,顺便学习这两种算法,可是这样以来问题就又来了,第一次写的双BFS(凭感觉写的)一提交就TLE了,当然这是第一次写的,错就错吧,水平问题。那我就先学习A*算法把,看了其他人的代码,发现这里有一个新的知识点:八数码的逆序数问题,因为这个问题决定了整个程序的效率问题,否则,不管你用的什么搜索,只要是遇到无解的情况时,都得搜个遍,那么何来谁更快的说法呢!到了这里,我似乎觉得我第一次写的双BFS TLE的原因了:没有判断是否有解!
这里添加一些关于八数码的逆序数问题的知识点:原文链接:http://blog.csdn.net/ju136/article/details/6876647
对于给定八数码棋局的初始状态,我们的目标是通过交换空格与其相邻棋子使棋盘达到目标状态。
其中,游戏规则是只能交换空格与其上下左右四个方向的相邻棋子。
假设棋局目标状态为如下形式:(A、B、C、D、E、F、G、H表示棋子)
A B C
D E F
G H
而初始状态就是A、B、C、D、E、F、G、H这八个棋子在这九个棋格上的任意分布。
并且我们对棋盘中每个棋格进行如下形式的编号:
1 2 3
4 5 6
7 8 9
那么,对于一个任意的棋局状态,我们可以取得这八个棋子(A、B、C、D、E、F、G、H)的一个数列:棋子按照棋格的编号依次进行排列,记为p=c[1]c[2]c[3]c[4]c[5]c[6]c[7]c[8](即A、B、C、D、E、F、G、H的一个排列)。
在分析之前,先引进逆序和逆序数的概念:对于棋子数列中任何一个棋子c[i](1≤i≤8),如果有j>i且c[j]
现在,我们对一个任意的棋局状态p=c[1]c[2]c[3]c[4]c[5]c[6]c[7]c[8]进行分析:
引理1:如果交换任何两个相邻的棋子,那么棋子数列的逆序数将发生奇偶性互变(奇偶性互变是指由奇数变为偶数,或由偶数变为奇数,下同)。
其证明很简单,假设交换的是c[i]和c[i+1],那么对于c[j](1≤j≤i-1或i+2≤j≤8)的逆序数并不改变。若交换之前 c[i]
引理2:如果棋子数列经过n次相邻棋子交换后,若n为偶数,则数列逆序数奇偶性不变;若n为奇数,则数列逆序数将发生奇偶性互变。
其证明可以由引理1简单推出。
引理3:在满足上述约定的八数码问题中,空格与相邻棋子的交换不会改变棋局中棋子数列的逆序数的奇偶性。
证明:显然空格与左右棋子交换不会改变棋子数列的逆序数(因为数列并没有改变)。现在考虑空格与上下棋子交换的情况:若空格与上方的棋子交换(假设交换是可行的),将得到一个新数列。若假设交换棋子为c[i]=X,那么原数列p=c[1]...X c[i+1]c[i+2]...c[8]将变为新数列q=c[1]...c[i+1]c[i+2]X ...c[8](注意:在棋盘中,上下相邻的两棋格之间隔有两个棋格)。由原数列p到新数列q的转变可以通过如下方式加以解释:用X与c[i+1]、 c[i+2]先后进行两次相邻交换而完成状态转变。所以根据引理2知,由p状态到q状态并不会改变改变棋子数列的逆序数的奇偶性。同理可证空格与下方棋子交换也不会改变棋子数列的逆序数的奇偶性。所以,空格与相邻棋子的交换不会改变棋局中棋子数列的逆序数的奇偶性。
定理1
(1)当初始状态棋局的棋子数列的逆序数是奇数时,八数码问题无解;
(2)当初始状态棋局的棋子数列的逆序数是偶数时,八数码问题有解。
证明:由引理3知,按照八数码问题的游戏规则,在游戏过程中,棋局的棋子数列的逆序数的奇偶性不会发生变化。而上面规定的目标状态没有逆序存在,所以目标状态下棋局的逆序数为偶数(实际为0)。显然,可能的初始状态棋局的棋子数列的逆序数可能为奇数,也可能为偶数(因为把一个初始状态中任意相邻两个棋子交换,得到的新的状态作为初始状态,它们的奇偶性相反)。所以,对于任意一个初始状态,若其棋局的棋子数列的逆序数为奇数,则永远也不可能达到目标状态,即八数码问题无解;若其棋局的棋子数列的逆序数为偶数,(接下来如何证明)。
A*算法代码:
#include
#include
#include
#include
#include
#include
#include
using namespace std;
struct node{
int s[9];
int cur,n;//分别表示0的位置,和当前hash值(即cantor函数的值)
int f,g,h;//即A*算法的f,g,h
node(){}
bool operator <(const node &a)const{
if(a.f!=f) return a.f q;
q.push(o);
vis[o.n]=1;
int ans=46233;//“123456780“的hash值
while(!q.empty())
{
node tmp=q.top();
if(tmp.n==ans){
disp(ans);
cout<2 || ty>2) continue;
node tmp2=tmp;
tmp2.cur=tx*3+ty;
swap(tmp2.s[tmp.cur],tmp2.s[tmp2.cur]);
tmp2.n=cantor(tmp2.s);
if(vis[tmp2.n]) continue;
vis[tmp2.n]=1;
front[tmp2.n]=tmp.n;
path[tmp2.n]=dir[p];
tmp2.g++;tmp2.h=get_h(tmp2.s);tmp2.f=tmp2.g+tmp2.h;
q.push(tmp2);
}
}
//cout<<"No\n";
}
int main(){
char ch;
while(cin>>ch){
if(ch=='x'){
ch='0';
o.cur=0;
}
o.s[0]=ch-'0';
for(int i=1;i<9;i++){
cin>>ch;
if(ch=='x'){
ch='0';
o.cur=i;
}
o.s[i]=ch-'0';
}
o.n=cantor(o.s);
o.g=0;o.h=get_h(o.s);o.f=o.g+o.h;
int cnt=0;
for(int i=0;i<8;i++)//记录逆序数
if(o.s[i])
for(int j=i+1;j<9;j++)
if(o.s[j]
双BFS代码:
这里我有个疑惑:既然双BFS可行,那么必然存在某一个状态到目标状态的层次最深,不妨设为Max,由于是前后一起遍历,前后分别遍历的最大层次为Max/2(Max为偶数)或 Max/2+1(Max为奇数),所以当下面程序中的 L>Max/2+1 时,结束while表示无解不就行了吗?这样一来不久可以不用进行八数码的逆序数无解判断了吗?(虽然这样更快)
可事实并不是这样,提交时,L=13时,WA,L=14时,TLE。下面这个代码时间是1.2s左右。这个疑惑还待解决哦!!!
当然,写到这里,我忽然想到了既然都是搜索,是否可以用双A*算法呢?效率如何呢?
A*是把层次变窄了,所以较快,双A*和单A*应该差不多,因为那和单 A*相比,在路径上没有什么改变,仅仅hi该边了遍历的方向。
#include
#include
#include
#include
#include
#include
#include
using namespace std;
int fac[]={1,1,2,6,24,120,720,5040,40320,362880};
int vis1[363000],vis2[363000];
int ed[]={1,2,3,4,5,6,7,8,0};
string path[363000];//记录路径
int cantor(int s[]){//康托展开,进行状态压缩
int sum=0;
for(int i=0;i<9;i++){
int cnt=0;
for(int j=i+1;j<9;j++)
if(s[j] q,qq;
q.push(o);
vis1[o.n]=1;
path[o.n]="";
memcpy(o.s,ed,sizeof(o.s));
o.n=cantor(o.s);
o.cur=8;
o.deep=0;
path[o.n]="";
qq.push(o);
vis2[o.n]=1;
int L=0;//记录层次,既然是双向的,肯定是你一层,我一层的顺序依次进行
while(!q.empty() || !qq.empty()){
while(q.front().deep==L){
node tmp=q.front();
q.pop();
int x=tmp.cur/3,y=tmp.cur%3;
for(int p=0;p<4;p++){
int tx=x+d[p][0],ty=y+d[p][1];
if(tx<0 || ty<0 || tx>2 || ty>2) continue;
node tmp2=tmp;
tmp2.cur=tx*3+ty;
swap(tmp2.s[tmp.cur],tmp2.s[tmp2.cur]);
tmp2.n=cantor(tmp2.s);
tmp2.deep=tmp.deep+1;
if(vis2[tmp2.n]){//这里判断相遇,如果逆序遍历已经达到了这里,则路径连通,找到了解
reverse(path[tmp2.n].begin(),path[tmp2.n].end());//这里得注意相遇点路径的记录
cout<2 || ty>2) continue;
node tmp2=tmp;
tmp2.cur=tx*3+ty;
swap(tmp2.s[tmp.cur],tmp2.s[tmp2.cur]);
tmp2.n=cantor(tmp2.s);
tmp2.deep=tmp.deep+1;
if(vis1[tmp2.n]){
reverse(path[tmp.n].begin(),path[tmp.n].end());
cout<>ch){
if(ch=='x') {
ch='0';
o.cur=0;
}
o.s[0]=ch-'0';
for(int i=1;i<9;i++){
cin>>ch;
if(ch=='x') {
ch='0';
o.cur=i;
}
o.s[i]=ch-'0';
}
o.deep=0;o.n=cantor(o.s);
int cnt=0;
for(int i=0;i<8;i++)//记录逆序数
if(o.s[i])
for(int j=i+1;j<9;j++)
if(o.s[j]