【算法系列学习三】[kuangbin带你飞]专题二 搜索进阶 之 A-Eight 反向bfs打表和康拓展开

时间:2024-05-02 16:36:01

[kuangbin带你飞]专题二 搜索进阶 之 A-Eight

这是一道经典的八数码问题。首先,简单介绍一下八数码问题:

八数码问题也称为九宫问题。在3×3的棋盘,摆有八个棋子,每个棋子上标有1至8的某一数字,不同棋子上标的数字不相同。棋盘上还有一个空格,与空格相邻的棋子可以移到空格中。要求解决的问题是:给出一个初始状态和一个目标状态,找出一种从初始转变成目标状态的移动棋子步数最少的移动步骤。所谓问题的一个状态就是棋子在棋盘上的一种摆法。棋子移动后,状态就会发生改变。解八数码问题实际上就是找出从初始状态到达目标状态所经过的一系列中间过渡状态。

八数码问题有多种解法,BFS,双向BFS,A*等等,可以参考八数码的八境界多种方法求解八数码问题.

求解这道题我用了两种方法:BFS+HASH(这种方法会TLE);改进之后的反向BFS打表+HASH。

整体思路:用BFS搜索,搜索过程中要保存当前棋盘状态,3*3的棋盘有9!中状态,可以考虑用康拓展开来压缩空间( 康托展开是一个全排列到一个自然数的映射,常用于构建哈希表时的空间压缩。 康托展开的实质是计算当前排列在所有由小到大全排列中的顺序,因此是可逆的。)目标状态(1 2 3 4 5 6 7 8 0)对应的自然数是46233。

方法一:

 #include<iostream>
#include<cstdio>
#include<string>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<queue>
//目标状态对应的自然数
#define END 46233
using namespace std;
//9!
const int maxn=;
//初始状态对应的自然数
int flag_star;
//判重
struct
{
int flag;//指向上一个状态
char dir;//从上一状体到这一状态的方向
}vis[maxn];
//阶乘,以求康拓展示开
int fact[];
//记录当前棋盘状态和x的位置
struct node
{
int a[];//当前棋盘的状态
int x;//x所在的位置
}; //递归输出路径 ,从目标状态往初始状态推 ,从初始状态往目标状态输出
void Print(int n)
{
if(n!=flag_star)
{
Print(vis[n].flag);
printf("%c",vis[n].dir);
}
}
//求阶乘
void Init()
{
fact[]=;
for(int i=;i<;i++)
{
fact[i]=fact[i-]*i;
}
}
//康拓展示
int Hash(int a[])
{
int ans=;
for(int i=;i<;i++)
{
int temp=;
for(int j=i+;j<;j++)
{
if(a[j]<a[i])
{
temp++;
}
}
ans+=temp*fact[-i];
}
return ans;
}
//str和dir是相对应的
char str[]="udlr";
int dir[][]={{-,},{,},{,-},{,}}; int bfs(node star)
{
queue<node> Q;
Q.push(star);
while(!Q.empty())
{
node q=Q.front();
Q.pop();
int flag=Hash(q.a);
if(flag==END)
{
return flag;
}
int pos=q.x;
for(int i=;i<;i++)
{
int xpos=q.x/;
int ypos=q.x%;
int xx=xpos+dir[i][];
int yy=ypos+dir[i][];
if(xx>=&&xx<&&yy>=&&yy<)
{
int now=xx*+yy;
swap(q.a[now],q.a[pos]);
q.x=now;
int v=Hash(q.a);
if(v==END)
{
vis[v].dir=str[i];
vis[v].flag=flag;
return v;
}
if(vis[v].flag==-)
{
vis[v].dir=str[i];
vis[v].flag=flag;
Q.push(q);
}
//回退
q.x=pos;
swap(q.a[now],q.a[pos]);
}
}
}
return maxn;
} int main()
{
Init();
char c[]; while(~scanf("%s",c))
{
for(int i=;i<maxn;i++)
{
vis[i].flag=-;
}
node star;
if(c[]=='x')
{
star.a[]=;
star.x=;
}
else
{
star.a[]=c[]-'';
}
for(int i=;i<;i++)
{
scanf("%s",c);
if(c[]=='x')
{
star.x=i;
star.a[i]=;
}
else
{
star.a[i]=c[]-'';
}
}
//初始状态
flag_star=Hash(star.a);
//特判,如果已经到达最后状态
if(flag_star==END)
{
printf("\n");
continue;
}
int f=bfs(star);
if(f==END)
{
Print(f);
printf("\n");
}
else
{
printf("unsolvable\n");
}
}
return ;
}

直接BFS

方法二:

因为有多个输入,直接BFS会T,所以可以反向BFS预处理,以目标状态为起点搜出所以可以到达的状态。

 #include<iostream>
#include<cstdio>
#include<string>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<queue>
//目标状态对应的自然数
#define END 46233
using namespace std;
//9!
const int maxn=;
//初始状态对应的自然数
int flag_star;
//判重
struct
{
int flag;//指向上一个状态
char dir;//从上一状体到这一状态的方向
}vis[maxn];
//阶乘,以求康拓展示开
int fact[];
//记录当前棋盘状态和x的位置
struct node
{
int a[];//当前棋盘的状态
int x;//x所在的位置
}; //递归输出路径 ,从目标状态往初始状态推 ,从初始状态往目标状态输出
void Print(int n)
{
if(n!=END)
{
printf("%c",vis[n].dir);
Print(vis[n].flag);
}
}
//求阶乘
void Init()
{
fact[]=;
for(int i=;i<;i++)
{
fact[i]=fact[i-]*i;
}
}
//康拓展示
int Hash(int a[])
{
int ans=;
for(int i=;i<;i++)
{
int temp=;
for(int j=i+;j<;j++)
{
if(a[j]<a[i])
{
temp++;
}
}
ans+=temp*fact[-i];
}
return ans;
}
//str和dir是相对应的
//char str[5]="udlr";
char str[]="durl";
int dir[][]={{-,},{,},{,-},{,}}; void bfs()
{
node star;
//以目标状态(1 2 3 4 5 6 7 0)为起点
for(int i=;i<;i++)
{
star.a[i]=i+;
}
star.a[]=;
star.x=;
queue<node> Q;
Q.push(star);
while(!Q.empty())
{
node q=Q.front();
Q.pop();
int flag=Hash(q.a);
int pos=q.x;
for(int i=;i<;i++)
{
int xpos=q.x/;
int ypos=q.x%;
int xx=xpos+dir[i][];
int yy=ypos+dir[i][];
if(xx>=&&xx<&&yy>=&&yy<)
{
int now=xx*+yy;
swap(q.a[now],q.a[pos]);
q.x=now;
int v=Hash(q.a);
if(vis[v].flag==-)
{
vis[v].dir=str[i];
vis[v].flag=flag;
Q.push(q);
}
//回退
q.x=pos;
swap(q.a[now],q.a[pos]);
}
}
}
} int main()
{
Init();
char c[];
for(int i=;i<maxn;i++)
{
vis[i].flag=-;
}
bfs();
while(~scanf("%s",c))
{ node star;
if(c[]=='x')
{
star.a[]=;
star.x=;
}
else
{
star.a[]=c[]-'';
}
for(int i=;i<;i++)
{
scanf("%s",c);
if(c[]=='x')
{
star.x=i;
star.a[i]=;
}
else
{
star.a[i]=c[]-'';
}
}
//初始状态
flag_star=Hash(star.a);
//特判,如果已经到达最后状态
if(flag_star==END)
{
printf("\n");
continue;
}
if(vis[flag_star].flag!=-)
{
Print(flag_star);
printf("\n");
}
else
{
printf("unsolvable\n");
}
}
return ;
}

有几点要注意的地方:

1.因为以目标状态为起点,方向正好反了

 //char str[5]="udlr";
char str[]="durl";
int dir[][]={{-,},{,},{,-},{,}};

方向要变

2.输出要变,直接从当前状态外目标状态推

 void Print(int n)
{
// if(n!=flag_star)
// {
// Print(vis[n].flag);
// printf("%c",vis[n].dir);
// }
if(n!=END)
{
printf("%c",vis[n].dir);
Print(vis[n].flag);
}
}

3.判断是否有解是输入状态是否在bfs的时候被经过。

 if(vis[flag_star].flag!=-)
{
Print(flag_star);
printf("\n");
}
else
{
printf("unsolvable\n");
}

方法三:参考别人的代码。A*算法。

最关键的部分是估价函数和判断逆序是否为偶数的剪枝

估价函数,是根据与目标解的曼哈顿距离,也就是每个数字与目标位置的曼哈顿距离之和。

 #include<iostream>
#include<cstdio>
#include<cstring>
#include<queue>
#include<cmath>
using namespace std; struct node //状态
{
int a[];
int f, h, g;
int x; //x在的位置 // bool operator<(const node n1)const{ //优先队列第一关键字为h,第二关键字为g
// return h!=n1.h?h>n1.h:g>n1.g;
// }
friend bool operator < (node a, node b)
{
return a.f > b.f;
}
}; priority_queue<node>que;
int fac[];
//
struct
{
int father;
char dir;
}vis[]; int get_h(int a[])
{
int h = ;
for(int i = ; i < ; i++)
{
if(a[i])
h += fabs((a[i]-)/ - i/) + fabs((a[i]-)% - i%);
}
return h;
} int Hash(int a[])
{
int ans = ;
for(int i = ; i < ; i++)
{
int tmp = ;
for(int j = i+; j < ; j++)
{
if(a[i] > a[j]) tmp++;
}
ans += tmp*fac[-i];
}
return ans+;
} void prin(int n)
{
// printf("n=%d\n", n);
if(vis[n].father!=-)
{
prin(vis[n].father);
printf("%c", vis[n].dir);
}
} void SWAP(int &x, int &y)
{
int t = x;
x = y;
y = t;
} int dir[][] = { {, }, {-, }, {, -}, {, } };
char dd[] = "dulr"; bool is(int a[])
{
int ans = ;
for(int i = ; i < ; i++)
{
if(a[i])
for(int j = i+; j < ; j++)
{
if(a[i] > a[j] && a[j])
ans++;
}
}
return !(ans&);
} void debug(int a[])
{
for(int i = ; i < ; i++)
{
for(int j = ; j < ; j++)
{
printf("%d ", a[i*+j]);
}
printf("\n");
}
printf("\n");
} int bfs(node star)
{
while(!que.empty()) que.pop();
que.push( star );
star.h = get_h( star.a ); star.g = ;
star.f = star.g + star.h;
vis[ Hash( star.a ) ].father = -;
while(!que.empty())
{
node tmp = que.top();
que.pop();
int father = Hash(tmp.a); // printf("father=%d\n", father); debug(tmp.a); for(int i = ; i < ; i++)
{
int x = dir[i][] + tmp.x/;
int y = dir[i][] + tmp.x%;
if( <= x && x < && <= y && y < )
{
node s = tmp;
s.x = x*+y;
SWAP( s.a[ tmp.x ], s.a[ s.x ] );
s.g++;
s.h = get_h( s.a );
s.f = s.h + s.g;
int son = Hash(s.a);
// printf("tmp.x =%d s.x=%d\n", tmp.x, s.x);
// printf("son=%d\n", son); debug(s.a);
if(son == )
{
vis[ son ].father = father;
vis[ son ].dir = dd[i];
prin();printf("\n");
return ;
}
if(!vis[ son ].father && is(s.a))
{
vis[ son ].father = father;
vis[ son ].dir = dd[i];
que.push( s );
}
}
}
}
return ;
} int main(void)
{
int i;
fac[] = ;
for(i = ; i < ; i++) fac[i] = fac[i-]*i;
node star;
char in[];
// freopen("ou.txt", "w", stdout);
while(~scanf("%s", in))
{
memset(vis, , sizeof(vis));
if(in[] == 'x')
{
star.a[] = ;
star.x = ;
}
else star.a[] = in[] - '';
for(i = ; i < ; i++)
{
scanf("%s", in);
if(in[] == 'x')
{
star.a[i] = ;
star.x = i;
}
else star.a[i] = in[] - '';
}
if(!is(star.a))
{
printf("unsolvable\n");continue;
}
if(Hash(star.a) == ) {printf("\n"); continue;}
if(bfs(star))
{
printf("unsolvable\n");
}
}
return ;
}

A*