一、双向广搜
在一般的BFS问题中,我们都是单向BFS,即只从起点开始扩展,某一次扩展到终点 (终点第一次出队) 就结束。当搜索范围只在某个矩阵或图的内部,点数比较少的情况下 (如“走迷宫”一题),从起点搜到终点只需线性时间复杂度 (每个点只被便遍历一次),完全可以采用单向BFS且不会超时。但是当扩展次数越多,状态空间越庞大时 (状态空间指数增加),若还是按照单向BFS,中间遍历到的不同状态会非常多,效率极其低下 (如下图灰色区域)。
若我们采用双向广搜,即从起点向终点扩展的同时,也从终点向起点扩展,搜到的标志是两者均扩展到某个点,在搜索过程中遍历到的状态会少很多 (如下图绿色区域)。
在使用双向广搜时,朴素的做法是每次从起点扩展一层,再从终点扩展一层。还有一个小优化:可以比较从起点扩展来的和从终点扩展来的状态空间,哪个小就从哪边扩展一层。
二、字串变换
在最坏情况下,有 6 6 6 种变换规则,长度为 20 20 20 的字符串的每个位置都能应用这些规则,一共要扩展 10 10 10 次,状态空间达 12 0 10 120^{10} 12010。就算一个字符串只有一个位置能应用一个规则,扩展 10 10 10 次后状态空间也达 6 10 6^{10} 610。可见如果不用双向广搜必超时。
注意在从终点扩展时,字串变换的规则是颠倒的 ( B 1 → A 1 , B 2 → A 2 , . . . B_1\to A_1, B_2\to A_2,... B1→A1,B2→A2,...)。在对 string t
实行一次变换时,可以采用 t.substr(i, j)
函数得到从 t[i]
开始长度为 j j j 的子串,其中 j j j 可省略,默认截到字符串末尾。
代码实现:
#include <iostream>
#include <algorithm>
#include <cstring>
#include <string>
#include <unordered_map>
#include <queue>
using namespace std;
const int N = 6;
int n;
string A, B;
string a[N], b[N];
int extend(queue <string> &q, unordered_map <string, int> &da, unordered_map <string, int> &db, string a[], string b[]){
int d = da[q.front()];
while (q.size() && da[q.front()] == d){
//注意一次是扩展一层,不是只扩展队头
string t = q.front();
q.pop();
for (int i = 0; i < t.size(); i ++)
for (int j = 0; j < n; j ++)
if (t.substr(i, a[j].size()) == a[j]){
string state = t.substr(0, i) + b[j] + t.substr(i + a[j].size());
if (db.count(state)) return da[t] + 1 + db[state]; //记得+1
if (da.count(state)) continue;
da[state] = da[t] + 1;
q.push(state);
}
}
return 11;
}
int bfs(string A, string B){
if (A == B) return 0;
queue <string> qa, qb;
unordered_map <string, int> da, db; //存储到起点、终点的变换次数,不用从1开始
qa.push(A), qb.push(B);
da[A] = db[B] = 0;
int step = 0;
while (qa.size() && qb.size()){