题目描述
洛谷P1379 八数码难题 (本博客的代码和描述都是针对洛谷这题)
问题 1426: [蓝桥杯][历届试题]九宫重排 (与洛谷那题 [基本] 一样,输入有所不同)
在3×3的棋盘上,摆有八个棋子,每个棋子上标有1至8的某一数字。棋盘中留有一个空格,空格用0来表示。空格周围的棋子可以移到空格中。要求解的问题是:给出一种初始布局(初始状态)和目标布局(为了使题目简单,设目标状态为123804765),找到一种最少步骤的移动方法,实现从初始布局到目标布局的转变。
输入格式
输入初始状态,一行九个数字,空格用0表示
输出格式
只有一行,该行只有一个数字,表示从初始状态到目标状态需要的最少移动次数(测试数据中无特殊无法到达目标状态数据)
输入输出样例
略
前言
最近在学数据结构,重新看回刘佳汝《算法竞赛入门经典》里面的八数码问题。我发现,洛谷、蓝桥杯、和我学校oj (scnuoj)上都有这个题。于是这个周末,
我闲着无聊,我打了好几个版本的代码。3个oj上我都提交过,都过了。趁现在脑还热就感觉下一篇博客记录一下。先说明一下,下面我只会说算法思路,具体的代码细节我就不赘述了,我贴出AC代码,希望能给各位一点帮助!由于我在不同oj上提交,代码会有些改动,我不知道会不会搞混了。如有错误,请各位指正。
版本1
单向bfs + stl set容器判重
(洛谷)总用时:7.53s
但过不了蓝桥杯那题,那题的数据点比洛谷强。。。
#include<iostream>
#include<cstdio>
#include<queue>
#include<cstring>
#include<set>
using namespace std;
const int d[][2]={{-1,0}, {0,1}, {1,0}, {0,-1}}; //上右下左
//bfs的状态结点
typedef struct State
{
char state[15];
int step;
State() { step=0; }
State(char* s, int cnt)
{
strcpy(state, s);
step=cnt;
}
}State;
State origin; //初始状态
char dest[]="123804765"; //最终状态
set<string> st;
void bfs()
{
queue<State> q;
q.push(origin);
while(!q.empty())
{
State head=q.front();
q.pop();
//判断是否已经达到最终状态
if(!strcmp(head.state, dest))
{
printf("%d\n", head.step);
return;
}
//找到空格的位置
int pos;
for(int i=0; head.state[i]!='\0'; i++)
if(head.state[i]=='0')
{
pos=i;
break;
}
int x=pos/3;
int y=pos%3;
for(int i=0;i<4;i++)
{
int x1=x+d[i][0];
int y1=y+d[i][1];
if(x1>=0 && x1<3 && y1>=0 && y1<3)
{
int pos1=x1*3+y1;
char s[15]; //扩展的新节点
strcpy(s, head.state);
swap(s[pos], s[pos1]);
if(!st.count(string(s))) //判断扩展的新状态是否已经访问过
{
st.insert(string(s));
q.push(State(s, head.step+1));
}
}
}
}
printf("-1\n");
}
int main()
{
scanf("%s", origin.state);
bfs();
return 0;
}
版本2
单向bfs + 字典树判重
将判重和插入分开:(洛谷)总用时:3.38s
在判重的同时实现插入:(洛谷)总用时:2.59s
《算法竞赛入门经典》里面是以整数形式存储每种状态,我以字符串形式存储,感觉操作方便一点
手写字典树,如非必要,不要装×。在确保了我的bfs主算法正确后,我才试着手写字典树的,虽然心里还是有点虚。但很庆幸,调了2次就过了。之前师兄曾给我们展示过一个用数组实现字典树的模板,但由于这学期学数据结构,老师介绍了树的左兄弟右孩子表示法,于是我就试着用链表实现字典树。
#include<iostream>
#include<cstdio>
#include<queue>
#include<cstring>
#include<set>
using namespace std;
const int d[][2]={{-1,0}, {0,1}, {1,0}, {0,-1}}; //上右下左
//bfs的状态结点
typedef struct State
{
char state[15];
int step;
State() { step=0; }
State(char* s, int cnt)
{
strcpy(state, s);
step=cnt;
}
}State;
State origin; //初始状态
char dest[]="123804765"; //最终状态
typedef struct Node
{
char c;
int cnt; //以该字符结尾的[前缀]出现的次数
Node* child; //左孩子有兄弟表示法
Node* brother;
Node()
{
cnt=0;
child=NULL;
brother=NULL;
}
}Node;
class Trie
{
public:
Trie();
int insert(char* s);
int find(char* s);
private:
Node* root;
};
Trie::Trie()
{
root=new Node;
}
int Trie::insert(char* s)
{
Node* u=root;
Node* v=NULL;
int success=0;
for(int i=0; s[i]!='\0'; i++)
{
int flag=0;
for(v=u->child; v!=NULL; v=v->brother)
if(v->c==s[i])
{
v->cnt+=1;
flag=1;
break;
}
if(!flag)
{
success=1;
v=new Node;
v->c=s[i];
v->child=NULL;
v->brother=u->child;
v->cnt=1;
u->child=v;
}
u=v;
}
return success;
}
int Trie::find(char* s)
{
Node* u=root;
Node* v=NULL;
for(int i=0; s[i]!='\0'; i++)
{
int flag=0;
for(v=u->child; v!=NULL; v=v->brother)
if(v->c==s[i])
{
flag=1;
break;
}
if(!flag)
return 0;
u=v;
}
return u->cnt;
}
Trie trie;
void bfs()
{
queue<State> q;
q.push(origin);
while(!q.empty())
{
State head=q.front();
q.pop();
//判断是否已经达到最终状态
if(!strcmp(head.state, dest))
{
printf("%d\n", head.step);
return;
}
//找到空格的位置
int pos;
for(int i=0; head.state[i]!='\0'; i++)
if(head.state[i]=='0')
{
pos=i;
break;
}
int x=pos/3;
int y=pos%3;
for(int i=0;i<4;i++)
{
int x1=x+d[i][0];
int y1=y+d[i][1];
if(x1>=0 && x1<3 && y1>=0 && y1<3)
{
int pos1=x1*3+y1;
char s[15]; //扩展的新节点
strcpy(s, head.state);
swap(s[pos], s[pos1]);
if(trie.insert(s)) //在判重的同时实现插入
q.push(State(s, head.step+1));
/*
if(!trie.find(s)) //先判重
{
trie.insert(s); //再插入
q.push(State(s, head.step+1));
}
*/
}
}
}
printf("-1\n");
}
int main()
{
scanf("%s", origin.state);
bfs();
return 0;
}
版本3
单向bfs + 手写哈希表判重
(洛谷)总用时:2.67s
在判重的同时进行插入,队列我用数组模拟,但效率没有明显提高,建议都用stl提供的队列。
额,是不是觉得我很无聊。又手写哈希表。。。这学期学Java,了解了HashSet的底层实现,于是就自己模仿Java的实现原理尝试用C++写个简单的哈希表。其实这也不是我第一次手写哈希表,23333。。。
在3个oj上实测,效率一般来说比字典树高,但不太稳定。哈希表的效率主要取决于哈希函数的优略和哈希表的大小。我用的这个字符串的哈希函数是从网上找的,别人测试过的。另外哈希表的大小1000003,最好不要动它,我试过我一旦动了它,用时就边长了。至少,假如你用这个哈希函数,这个哈希表的大小就建议用1000003!针对其他哈希函数我不知道。
#include<iostream>
#include<cstdio>
#include<queue>
#include<cstring>
#include<set>
using namespace std;
const int d[][2]={{-1,0}, {0,1}, {1,0}, {0,-1}}; //上右下左
//bfs的状态结点
typedef struct State
{
char state[15];
int step;
State() { step=0; }
State(char* s, int cnt)
{
strcpy(state, s);
step=cnt;
}
}State;
State origin; //初始状态
char dest[15]="123804765"; //最终状态
//set<string> st;
//哈希表的结点
const int maxn=1000003;
typedef long long ll;
typedef struct Node
{
char str[15];
Node* next;
Node() { }
Node(char* s, Node* nt)
{
strcpy(str, s);
next=nt;
}
}Node;
Node* hashTable[maxn]; //哈希表
//求哈希值并映射到哈希表的坐标
int BKDRHash(char *str)
{
ll seed = 131;
ll hash = 0;
while (*str)
hash = hash * seed + (*str++);
return (int)((hash & 0x7FFFFFFF)%maxn);
}
//0:表示该字符串已存在,插入失败 1:字符串不存在,插入成功
int tryInsert(char* s)
{
int hash=BKDRHash(s);
Node* p=hashTable[hash];
if(p==NULL)
hashTable[hash]=new Node(s, NULL); //注意不能写成 p=
else
{
while(p->next!=NULL)
{
if(!strcmp(p->str, s)) //已存在
return 0;
p=p->next;
}
p->next=new Node(s, NULL);
}
return 1;
}
State* q[maxn]; //模拟队列
int front=-1;
int rear=-1;
void bfs()
{
//queue<State> q;
//q.push(origin);
q[++rear]=&origin;
while(front<rear)
{
//State head=q.front();
//q.pop();
State* head=q[++front];
//判断是否已经达到最终状态
if(!strcmp(head->state, dest))
{
printf("%d\n", head->step);
return;
}
//找到空格的位置
int pos;
for(int i=0; head->state[i]!='\0'; i++)
if(head->state[i]=='0')
{
pos=i;
break;
}
int x=pos/3;
int y=pos%3;
for(int i=0;i<4;i++)
{
int x1=x+d[i][0];
int y1=y+d[i][1];
if(x1>=0 && x1<3 && y1>=0 && y1<3)
{
int pos1=x1*3+y1;
char s[15]; //扩展的新节点
strcpy(s, head->state);
swap(s[pos], s[pos1]);
if(tryInsert(s)) //不存在,插入成功
{
//q.push(State(s, head.step+1));
q[++rear]=new State(s, head->step+1);
}
/*
if(!st.count(string(s))) //判断扩展的新状态是否已经访问过
{
st.insert(string(s));
q.push(State(s, head.step+1));
}
*/
}
}
}
printf("-1\n");
}
int main()
{
scanf("%s", origin.state);
bfs();
return 0;
}
版本4
双向bfs + map标记
(洛谷)总用时:351ms
大一参加蓝桥杯省赛之前,师兄曾开过一场培训,那时师兄就介绍过双向bfs,当时也讲了哈希表。。但当时听个懵懵懂懂。双向bfs,就是从起点和从终点“同时”bfs,这个同时并不是真的同时,只是两棵bfs树交替向外扩展,相当于你扩展一层后,然后轮到我扩展一层。当两棵bfs树相遇,最短路为相遇的两个状态的步数之和+1。开一个队列也可以实现!
如何判断两棵bfs树相遇呢?这个标记就很巧妙了。。这个标记我是借鉴了其它题解的。
看的出来,综合考虑,在赛场上这是首选!代码简短,效率还高。
#include<iostream>
#include<cstdio>
#include<queue>
#include<cstring>
#include<map>
using namespace std;
const int d[][2]={{-1,0}, {0,1}, {1,0}, {0,-1}}; //上右下左
string origin; //初始状态
string dest="123804765"; //最终状态
map<string,int> vis;
map<string,int> step;
void bfs()
{
//特判
if(origin==dest)
{
printf("0");
return;
}
queue<string> q;
q.push(origin);
vis[origin]=1;
step[origin]=0;
q.push(dest);
vis[dest]=2;
step[dest]=0;
while(!q.empty())
{
string head=q.front();
q.pop();
int pos;
for(int i=0; i<head.length(); i++)
if(head[i]=='0')
{
pos=i;
break;
}
int x=pos/3;
int y=pos%3;
for(int i=0;i<4;i++)
{
int x1=x+d[i][0];
int y1=y+d[i][1];
if(x1>=0 && x1<3 && y1>=0 && y1<3)
{
int pos1=x1*3+y1;
string s=head; //copy一份
swap(s[pos], s[pos1]);
if(!vis.count(s))
{
q.push(s);
vis[s]=vis[head];
step[s]=step[head]+1;
}
else if(vis[s]+vis[head]==3)
{
printf("%d", step[s]+step[head]+1);
return;
}
}
}
}
printf("-1"); //这个没用,因为题目说一定可达。。但蓝桥杯那题有不可达的情况
}
int main()
{
cin>>origin;
bfs();
return 0;
}
版本5
双向bfs优化 + map判重
(洛谷)总用时:358ms
每次出队,元素少的那个队列的对头元素出队! 所有只能开两个队列了。
详情见大神博客:https://blog.youkuaiyun.com/ww32zz/article/details/50755225
好像用时没有减少。。但我在蓝桥杯题库和学校oj上提交,用时少了一点点。原因我盲猜一下,造成两个队列里面元素个数不相等的原因,就是其中一个bfs在扩展状态结点时碰到边界了。所以这个优化是否明显还要却决于两个bfs的起点的位置。蓝桥杯那题的终点状态不是固定的,可能这个优化对于蓝桥杯那题会比较明显吧。。快了十几ms。。如果我没记错。。当然,上述纯是我盲猜。。。也有可能我代码写错了,所以不明显。。
#include<iostream>
#include<cstdio>
#include<queue>
#include<cstring>
#include<map>
using namespace std;
const int d[][2]={{-1,0}, {0,1}, {1,0}, {0,-1}}; //上右下左
string origin; //初始状态
string dest="123804765"; //最终状态
map<string,int> vis;
map<string,int> step;
void bfs()
{
//特判
if(origin==dest)
{
printf("0");
return;
}
queue<string> q1;
queue<string> q2;
q1.push(origin);
vis[origin]=1;
step[origin]=0;
q2.push(dest);
vis[dest]=2;
step[dest]=0;
while(!q1.empty() || !q2.empty())
{
string head;
int flag;
if(q1.size()<q2.size())
{
head=q1.front();
q1.pop();
flag=1;
}
else
{
head=q2.front();
q2.pop();
flag=2;
}
int pos;
for(int i=0; i<head.length(); i++)
if(head[i]=='0')
{
pos=i;
break;
}
int x=pos/3;
int y=pos%3;
for(int i=0;i<4;i++)
{
int x1=x+d[i][0];
int y1=y+d[i][1];
if(x1>=0 && x1<3 && y1>=0 && y1<3)
{
int pos1=x1*3+y1;
string s=head; //copy一份
swap(s[pos], s[pos1]);
if(!vis.count(s))
{
if(flag==1)
q1.push(s);
else if(flag==2)
q2.push(s);
vis[s]=vis[head];
step[s]=step[head]+1;
}
else if(vis[s]+vis[head]==3)
{
printf("%d", step[s]+step[head]+1);
return;
}
}
}
}
}
int main()
{
cin>>origin;
bfs();
return 0;
}
版本6 终极版本
双向bfs优化 + 字典树判重
(洛谷)总用时:178ms
由于在使用双向bfs时要进行特殊标记,所以字典树要进行改动!具体实现我就不赘述了,参见代码。
在学校oj测试,最高用时:16ms
在蓝桥杯题库,最高用时:23ms
#include<iostream>
#include<cstdio>
#include<queue>
#include<cstring>
#include<set>
using namespace std;
const int d[][2]={{-1,0}, {0,1}, {1,0}, {0,-1}}; //上右下左
string origin; //初始状态
string dest="123804765"; //最终状态
typedef struct Node
{
char c;
int cnt; //以该字符结尾的[前缀]出现的次数。这个没用。。但顺手写上去
int flag; //这个flag标记只有在字典树的叶子结点才被标记,非叶子节点这个flag相当于个冗余字段
int step;
Node* child; //左孩子有兄弟表示法
Node* brother;
Node()
{
cnt=0;
flag=0;
step=0;
child=NULL;
brother=NULL;
}
}Node;
class Trie
{
public:
Trie();
int insert(string s, int flag, int step);
Node* find(string s);
private:
Node* root;
};
Trie::Trie()
{
root=new Node;
}
int Trie::insert(string s, int flag, int step)
{
Node* u=root;
Node* v=NULL;
int success=0;
for(int i=0; i<s.length(); i++)
{
int flag=0;
for(v=u->child; v!=NULL; v=v->brother)
if(v->c==s[i])
{
v->cnt+=1;
flag=1;
break;
}
if(!flag)
{
success=1;
v=new Node;
v->c=s[i];
v->child=NULL;
v->brother=u->child;
v->cnt=1;
u->child=v;
}
u=v;
}
u->flag=flag;
u->step=step;
return success;
}
Node* Trie::find(string s)
{
Node* u=root;
Node* v=NULL;
for(int i=0; i<s.length(); i++)
{
int flag=0;
for(v=u->child; v!=NULL; v=v->brother)
if(v->c==s[i])
{
flag=1;
break;
}
if(!flag)
return NULL;
u=v;
}
return u;
}
Trie trie;
void bfs()
{
//特判
if(origin==dest)
{
printf("0");
return;
}
queue<string> q1;
queue<string> q2;
q1.push(origin);
trie.insert(origin, 1, 0);
q2.push(dest);
trie.insert(dest, 2, 0);
while(!q1.empty() || !q2.empty())
{
string head;
int flag;
if(q1.size()<=q2.size())
{
head=q1.front();
q1.pop();
flag=1;
}
else
{
head=q2.front();
q2.pop();
flag=2;
}
int pos;
for(int i=0; i<head.length(); i++)
if(head[i]=='0')
{
pos=i;
break;
}
int x=pos/3;
int y=pos%3;
for(int i=0;i<4;i++)
{
int x1=x+d[i][0];
int y1=y+d[i][1];
if(x1>=0 && x1<3 && y1>=0 && y1<3)
{
int pos1=x1*3+y1;
string s=head; //copy一份
swap(s[pos], s[pos1]);
Node* h=trie.find(head);
Node* t=trie.find(s);
if(t==NULL)
{
if(flag==1)
q1.push(s);
else if(flag==2)
q2.push(s);
trie.insert(s, h->flag, h->step+1);
}
else if(t->flag + h->flag==3)
{
printf("%d", t->step + h->step + 1);
return;
}
}
}
}
}
int main()
{
cin>>origin;
bfs();
return 0;
}