双向广度优先搜索:八数码难题
八数码难题
题目描述
在 3 × 3 3\times 3 3 × 3 的棋盘上,摆有八个棋子,每个棋子上标有 1 1 1 至 8 8 8 的某一数字。棋盘中留有一个空格,空格用 0 0 0 来表示。空格周围的棋子可以移到空格中。要求解的问题是:给出一种初始布局(初始状态)和目标布局(为了使题目简单,设目标状态为 123804765 123804765 1 2 3 8 0 4 7 6 5 ),找到一种最少步骤的移动方法,实现从初始布局到目标布局的转变。
输入格式
输入初始状态,一行九个数字,空格用 0 0 0 表示。
输出格式
只有一行,该行只有一个数字,表示从初始状态到目标状态需要的最少移动次数。保证测试数据中无特殊无法到达目标状态数据。
样例 #1
样例输入 #1
样例输出 #1
提示
样例解释
图中标有 0 0 0 的是空格。绿色格子是空格所在位置,橙色格子是下一步可以移动到空格的位置。如图所示,用四步可以达到目标状态。
并且可以证明,不存在更优的策略。
双向广度优先搜索:
我们常常会面临这样一类搜索问题:起点是给出的,终点也是已知的,需要确定能否从起点到达终点,如果可以,需要多少步。
如果我们用常规的搜索方法,从起点开始往下搜,那得到的解答树可能非常庞大,这样漫无目的的搜索就像大海捞针 。
要从庞大的解答树中找到一根“针”,即终点,用蓝色表示
让我们切换一下思路,既然终点是已知的,我们何必让它闲着呢?我们完全可以分别 从起点和终点出发,看它们能否相遇 :
从起点和终点分别展开搜索,解答树瞬间缩小了很多
如果原本的解答树规模是 ,使用双向搜索后,规模立刻缩小到了约 ,当 较大时优化非常可观。
双向搜索主要有两种,双向BFS和双向迭代加深。
双向BFS
与普通的BFS不同,双向BFS维护两个 而不是一个 队列,然后轮流 拓展两个队列。同时,用数组 (如果状态可以被表示为较小的整数)或哈希表 记录当前的搜索情况,给从两个方向拓展的节点以不同的标记。当某点被两种标记同时标记时,搜索结束。
主要代码部分思想
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 queue <T> Q[3 ]; bool found = false ;Q[1 ].push(st); Q[2 ].push(ed); for (int d = 0 ; d < D + 2 ; ++d) { int dir = (d & 1 ) + 1 , sz = Q[dir].size(); for (int i = 0 ; i < sz; ++i) { auto x = Q[dir].front(); Q[dir].pop(); if (H[x] + dir == 3 ) found = true ; H[x] = dir; } if (found) }
本题代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 #include <iostream> #include <unordered_map> #include <queue> using namespace std ; #define int long long int dx[4 ]={1 ,-1 ,0 ,0 };int dy[4 ]={0 ,0 ,1 ,-1 };unordered_map <string ,int > H;queue <string > q[3 ];string justswap (string temp,int pos,int topos) { if (topos<pos) { int temp=topos; topos=pos; pos=temp; } string ret1=temp.substr(0 ,pos); string ret2=temp.substr(pos+1 ,topos-pos-1 ); string ret3=temp.substr(topos+1 ,temp.size()-topos); return ret1+temp[topos]+ret2+temp[pos]+ret3; } signed main (void ) { string st; string ed = "123804765" ; cin >>st; q[1 ].push(st); q[2 ].push(ed); int co=0 ; while (!q[1 ].empty() || !q[2 ].empty()) { int direct=co%2 +1 ; int qs=q[direct].size(); for (int i=0 ;i<qs;i++) { string temp=q[direct].front(); q[direct].pop(); if (H.count(temp)) { if (H[temp]+direct==3 ) { cout <<co-1 <<endl ; exit (0 ); } } else { H.insert({temp,direct}); int pos=temp.find('0' ); for (int i=0 ;i<=3 ;i++) { if ((pos%3 )+dx[i]<0 || (pos%3 )+dx[i]>2 || pos+dy[i]*3 <0 || pos+dy[i]*3 >=9 )continue ; string t=justswap(temp,pos,pos+dx[i]+dy[i]*3 ); q[direct].push(t); } } } co++; } return 0 ; }
一般形式(节选自oiwiki)(用一个队列写的方式,但是不适用于本题)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 将开始结点和目标结点加入队列 q 标记开始结点为 1 标记目标结点为 2 while (队列 q 不为空){ 从 q.front() 扩展出新的 s 个结点 如果 新扩展出的结点已经被其他数字标记过 那么 表示搜索的两端碰撞 那么 循环结束 如果 新的 s 个结点是从开始结点扩展来的 那么 将这个 s 个结点标记为 1 并且入队 q 如果 新的 s 个结点是从目标结点扩展来的 那么 将这个 s 个结点标记为 2 并且入队 q }
Harbin Institute of Technology, Shenzhen 计算机科学与技术 本科在读