一、 双向BFS
我们之前学过的广度优先搜索,如果将它应用于较大的图进行搜索或搜索分支较多的情况,那么它的效率就会变得很低,简单的BFS就很容易会超时,这个时候我们有一种速度更快的搜索算法叫做双向BFS。
双向BFS是一种图搜索算法,它从起点和终点同时开始搜索,以求得最短的路径。相比于普通的 BFS 算法,双向BFS算法可以减少搜索的节点数,从而提高搜索效率。
在双向 BES 算法中,我们使用两个队列或集合分别记录从起点和终点出发的可达节点,并在每一轮中交替地从两个队列或集合中选择一个节点进行扩展。如果某个节点在两个方向上都被访问到了,那么说明在这两个节点之间存在一条路径。
双向BFS的实现过程可以用下图来表示:
双向BFS的优势
双向BFS的优势与普通的BFS算法相比具有以下优势:
1.搜索的节点数更少。由于双向 BFS 算法从两个方向同时开始搜索,因此它的搜索深度是普通 BFS 算法的一半,从而减少了搜索的节点数。
2.搜索的速度更快。由于双向 BFS 算法从起点和终点同时开始搜索,因此它的搜索速度更快。
3.能够避免无效搜索。由于双向 BFS 算法是从起点和终点同时开始搜索,因此它可以避免搜索那些无效的节点。
双向BFS的应用场景
双向BFS因为是从起点和终点同时开始搜索,所以我们只有当明确知道起点和终点的时候,我们才能使用双向BFS。把从起点到终点的单向搜索,变换为分别从起点和终点同时出发的“相遇”问题。或者是说当我们有两个点都需要同时开始移动的时候,我们也可以使用双向BFS。
双向BFS算法的实现步骤
1.定义两个队列Qstart和Qend,分别用于记录从起点和终点开始的可达节点。同时,定义两个哈希表mp1和mp2,用于记录每个状态或是结点是否被访问过。
2.将起点加入Qstart和mp1中,将终点加入Qend和mp2中。
3.为了保证从两个方向搜索的空间尽量均匀,我们选择队列大小较小的那个队列开始遍历。
4.如果某一方向的队列为空,则结束搜索。如果遍历到了同一个节点,证明已经找打了一条路径,也结束搜索。
5.如果搜索结束时没有找到终点,则说明不存在起点到终点的路径。
二、例题讲解-八数码难题
题目描述
在 的棋盘上,摆有八个棋子,每个棋子上标有 至 的某一数字。棋盘中留有一个空格,空格用 来表示。空格周围的棋子可以移到空格中。要求解的问题是:给出一种初始布局(初始状态)和目标布局(为了使题目简单,设目标状态为 ),找到一种最少步骤的移动方法,实现从初始布局到目标布局的转变。
输入格式
输入初始状态,一行九个数字,空格用 表示。
输出格式
只有一行,该行只有一个数字,表示从初始状态到目标状态需要的最少移动次数。保证测试数据中无特殊无法到达目标状态数据。
样例输入
283104765
样例输出
4
样例解释

图中标有 的是空格。绿色格子是空格所在位置,橙色格子是下一步可以移动到空格的位置。如图所示,用四步可以达到目标状态。
并且可以证明,不存在更优的策略。
解题思路
八数码难题这个问题他给了我们初始布局和目标布局,那么我们就可以用双向BFS来解这个题目:
第一步:定义队列和哈希表
queue<string> qstart, qend; //两个队列来枚举字符
unordered_map<string, int> mp1, mp2; //两个哈希表来储存已经走过的节点
第二步:将头和尾加入队列和哈希表
//把两端的初始点加入队列
qstart.push(sstart);
qend.push(send);
mp1[sstart] = 0;
mp2[send] = 0;
第三步:从层数少的开始遍历
while(qstart.size() && qend.size())//两个队列都不空才进行循环
{
int ans;
//让两个bfs的层数尽可能平均,从少的先遍历
if(qsart.size() > qend.size()){
ans = bfs(qend, mp2, mp1);
}else{
ans = bfs(qstart, mp1, mp2);
}
if(ans != -1) return ans;
}
第四步:遍历队列,查找是否存在路径
//参数一:我要枚举的队列。参数二:当前方向的map。参数三:对向方向的map。注意都要传入引用
//返回到的路径长度
int bffs(queue<string> &q, unordered_map<string,int> &mp1, unordered_map<string,int> &mp2)
{
string u = q.front();
q.pop();
PII uu = get_8(u);//拿到在九宫格中的坐标位置
for(int i = 00; i < 4; i++)
{
PII vv = {uu.x + dx[i], uu.y + dy[i]};//枚举交换的位置
if(vv.x < 0 || vv.x >= 3 || vv.y < 0 || vv.y >= 3) continue;
string v = u;
swap(v[uu.x * 3 + uu.y], v[vv.x * 3 + vv.y]);//将这两个位置交换也就是可扩展后的字符串
if(mp2.count(v)) return mp2[v] + 1;//如果在mp2中找到,则表示有一条路径了,直接返回路径长度
if(mp1.count(v)) continue;//如果在mp1中找到表示已经枚举过了,就不用枚举了
mp1[v] = mp1[u] + 1;//路径长度
q.push(v);//加入队列
}
return -1;//表示目前还没找到完整的路径
}
辅助函数,获得0在九宫格中的坐标
//将0点转化为坐标
PII get_0(string s){
for(int i = 0; i < 9; i++){
if(s[i] == '0') return {i / 3, i % 3};
}
}
将这四部分组合,就可以构成这道题的完整代码。
#include <bits/stdc++.h>
using namespace std;
typedef pair<string, int> PSI;
typedef pair<int, int> PII;
#define x first
#define y second
string sstart, send = "123804765";
int dx[] = {0, 1, 0, -1};
int dy[] = {1, 0, -1, 0};
//将0点转化为坐标
PII get_0(string s)
{
for(int i = 0; i < 9; i++)
{
if(s[i] == '0') return {i / 3, i % 3};
}
}
//参数一:我要枚举的队列。参数二:当前方向的map。参数三:对面方向的map。注意都要传入引用
//返回到的路径长度
int bbfs(queue<string> &q, unordered_map<string,int> &mp1, unordered_map<string,int> &mp2)
{
string u = q.front();
q.pop();
PII uu = get_0(u);//拿到在九宫格中的坐标位置
for(int i = 0; i < 4; i++)
{
PII vv = {uu.x + dx[i], uu.y + dy[i]};//枚举交换的位置
if(vv.x < 0 || vv.x >= 3 || vv.y < 0 || vv.y >= 3) continue;
string v = u;
swap(v[uu.x * 3 + uu.y], v[vv.x * 3 + vv.y]);//将这两个位置交换也就是可扩展后的字符串
if(mp2.count(v)) return mp2[v] + mp1[u] + 1;//如果在mp2中找到,则表示有一条路径了,直接返回
if(mp1.count(v)) continue;//如果在mp1中找到表示已经枚举过了,就不用枚举了
mp1[v] = mp1[u] + 1;//路径长度
q.push(v);//加入队列
}
return -1;//表示目前还没找到完整的路径
}
int bfs()
{
queue<string> qstart, qend;//两个队列来枚举字符
//两个哈希表来存储已经走过的状态
unordered_map<string, int> mp1, mp2;
//把两端的初始点加入队列
qstart.push(sstart);
qend.push(send);
mp1[sstart] = 0;
mp2[send] = 0;
while(qstart.size() && qend.size())//两个队列都不空才进行循环
{
int ans;
//让两个bfs的层数尽可能平均,从少的先遍历
if(qstart.size() > qend.size()) ans = bbfs(qend, mp2, mp1);
else ans = bbfs(qstart, mp1, mp2);
//不等于-1说明找到直接返回
if(ans!= -1) return ans;
}
return -1;
}
int main()
{
cin>>sstart;
if(sstart == send) cout<<0;
else cout<<bfs();
return 0;
}
三、例题讲解-字串变换
题目描述
已知有两个字串 A,B 及一组字串变换的规则(至多 6 个规则),形如:
- A1→B1。
- A2→B2。
规则的含义为:在 A 中的子串 A1 可以变换为 B1,A2 可以变换为 B2⋯。
例如:A=abcd,B=xyz,
变换规则为:
- abc→xu,ud→y,y→yz。
则此时,A 可以经过一系列的变换变为 B,其变换的过程为:
- abcd→xud→xy→xyz。
共进行了 3 次变换,使得 A 变换为 B。
输入格式
第一行有两个字符串 A,B。
接下来若干行,每行有两个字符串 Ai,Bi,表示一条变换规则。
输出格式
若在 10 步(包含 10 步)以内能将 A 变换为 B,则输出最少的变换步数;否则输出 NO ANSWER!。
输入输出样例
输入 #1复制
abcd xyz abc xu ud y y yz
输出 #1复制
3
说明/提示
对于 100% 数据,保证所有字符串长度的上限为 20。
解题思路
本题也是一道非常经典的双向BFS问题,已经给定我们初始字符串和目标字符串以及变换规则,那么我们就可以使用双向BFS来解决。
第一步:定义队列和哈希表
queue<string> qstart, qend; // 两个方向的队列
//每个状态到起点的距离mp1(哈希表),每个状态到终点的距离mp2哈希表
unordered_map<string, int> mp1, mp2;
第二步:将头和尾加入队列和哈希表
// qstart从起点开始搜,qend从终点开始搜
qstart.push(A), mp1[A] = 0; // 起点A到起点的距离为0
qend.push(B), mp2[B] = 0; // 终点B到终点B的距离为0
第三步:从层数少的开始遍历
while(qstart.size() && qend.size())
{
int ans; // 记录最小步数
// 哪个方向的队列的长度更小一些,空间更小一些,从该方向开始扩展,
if(qstart.size() <= qend.size())
ans = bbfs(qstart, mp1, mp2, a, b);
else
ans = bbfs(qend, mp2, mp1, b, a);
// 如果最小步数在10步以内
if(ans <= 10)
return ans;
}
return 11; // 如果不连通或者最小步数>10
第四步:遍历队列,查找是否存在路径(扩展函数)
// 扩展函数
// 参数:扩展的队列,到起点的距离,到终点的距离,规则,规则
// 返回值:满足条件的最小步数
int bbfs(queue<string>& q, unordered_map<string,int>& mp1,
unordered_map<string,int>& mp2,string a[], string b[])
{
string t = q.front();
q.pop();
for(int i = 0; i < t.size(); i++) // t从哪里开始扩展
{
for(int j = 0; j < n; j++) // 枚举规则
{
//如果t这个字符串的一段 = 规则,比如 = xyz,才可以替换
if(t.substr(i, a[j].size()) == a[j])
{
// 变换之后的结果state:前面不变的部分+ 变化的部分 + 后面不变的部分
// 比如abcd ,根据规则abc --> xu,变成 xud,这里的state就是xud
string state = t.substr(0,i) +b[j] + t.substr(i + a[j].size());
// state状态是否落到b里面去,两个方向会师,返回最小步数
if(mp2.count(state))
return mp1[t] + 1 + mp2[state];
// 如果该状态之前已扩展过,
if(mp1.count(state))
continue;
mp1[state] = mp1[t] + 1;
q.push(state);
}
}
}
return 11;
}
综上所述,组合四个部分的代码,就可以得出本题的完整代码:
#include<bits/stdc++.h>
using namespace std;
const int N = 6;
string a[N],b[N];
// 从起点和终点来做bfs
int bfs(string A, string B)
{
// 每个状态到起点的距离mp1(哈希表),每个状态到终点的距离mp2哈希表
queue<string> qstart, qend; // 两个方向的队列
unordered_map<string, int> mp1, mp2;
// qstart从起点开始搜,qend从终点开始搜
qstart.push(A), mp1[A] = 0; // 起点A到起点的距离为0
qend.push(B), mp2[B] = 0; // 终点B到终点B的距离为0
while(qstart.size() && qend.size())
{
int ans; // 记录最小步数
// 哪个方向的队列的长度更小一些,空间更小一些,从该方向开始扩展
if(qstart.size() <= qend.size())
ans = bbfs(qstart, mp1, mp2, a, b);
else
ans = bbfs(qend, mp2, mp1, b, a);
// 如果最小步数在10步以内
if(ans <= 10) return ans;
}
return 11; // 如果不连通或者最小步数>10,则返回大于10的数
}
// 扩展函数
// 参数:扩展的队列,到起点的距离,到终点的距离,规则,规则
// 返回值:满足条件的最小步数
int bbfs(queue<string>& q, unordered_map<string,int>& mp1,
unordered_map<string,int>& mp2,string a[], string b[])
{
string t = q.front();
q.pop();
for(int i = 0; i < t.size(); i++) // t从哪里开始扩展
{
for(int j = 0; j < n; j++) // 枚举规则
{
//如果t这个字符串的一段 = 规则,比如 = xyz,才可以替换
if(t.substr(i, a[j].size()) == a[j])
{
// 变换之后的结果state:前面不变的部分+ 变化的部分 + 后面不变的部分
// 比如abcd ,根据规则abc --> xu,变成 xud,这里的state就是xud
string state = t.substr(0,i) +b[j] + t.substr(i + a[j].size());
// state状态是否落到b里面去,两个方向会师,返回最小步数
if(mp2.count(state)) return mp1[t] + 1 + mp2[state];
// 如果该状态之前已扩展过
if(mp1.count(state)) continue;
mp1[state] = mp1[t] + 1;
q.push(state);
}
}
}
return 11;
}
int main()
{
string A, B;
cin >> A >> B;
if(A==B)
{
cout<<0;
return 0;
}
// 读入扩展规则分别存在a数组和b数组
while(cin >> a[n] >> b[n]) n++;
int step = bfs(A,B);
if(step > 10) cout<<"NO ANSWER!";
else cout<<step;
}

2693

被折叠的 条评论
为什么被折叠?



