广度优先搜索之双向BFS知识点总结

一、 双向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;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值