算法百题斩其二: 双向bfs——bfs的一种优化

算法百题斩其二: 双向bfs——bfs的一种优化

写在前面:何所谓“斩”?
斩,即快速而有力地切断,指我们用最精简的语言,一针见血地点破算法题的核心难点。斩需三思而后行;斩需借助外力、旁征博引;斩需持之以恒、铁杵磨针!
1. 何为双向bfs?

是啥?
知道初态和末态、且从两个方向产生的搜索树可覆盖所有(合法)状态的情况下,通过从起点和终点分别进行bfs,直到形成的两个连通块相交(若不交则无解),产生深度减半的搜索树。一般地,选择点集较小的那一侧进行下一次的延申操作,可以最小化“浪费”的状态,或者更直观地说可以使状态数量曲线(指数型)更加平滑。
有啥用?
可以显著降低时间复杂度(不难想象,这是指数级的优化!),对于一些状态数明显超标的题目,可以有效避免TLE!
这里转载一张图,摘自搜索优化技巧:双向搜索
在这里插入图片描述
如何不严谨地 证明一下?
见例题一代码注释~

例题一
在这里插入图片描述
在这里插入图片描述
分析+代码:
版本一:手写队列版本!

//使用双向bfs优化的最小步数模型,总状态太多,若直接bfs会超时。故使用双端bfs
//双端bfs求最短路的简单证明:假设某层遍历后,从起点延伸的点集和从终点延伸的点集没有会师,则说明不存在长度小于等于d[a]max+d[b]max的路线(反证),以此类推,第一次得到的一定是最短路
//要点:即使是双向bfs,无休止地遍历所有状态也会TLE,为了避免双端bfs遍历到所有状态,我们需要严格思考其是否保证在一定时候“停下来”
//比如本题中,假如两个字符串相等,我们的程序会忽略那个解而一直找下去,最终超时,故需要特判一下这种情况!!
//细节:1.利用map的count函数(判断某个自变量的键是否被赋值过),可以实现距离数组和判重数组的“合二为一”
//细节:2.从队长较短的那一侧进行当一轮的延申,可以最小化“浪费”的状态,或者更直观地说可以使状态数量曲线(指数型)更加平滑。
//细节:3.string类的substr函数的两种重载:
//substr(index_a,len)表示以下标a为起点,长度为len的子串
//substr(index_b)表示从下标b到字符串终点的子串
#include <iostream>
#include <queue>
#include <map>
#include <algorithm>

using namespace std;

const int N=7,M=5e4+10;
string rule_a[N],rule_b[N],A,B,qa[M],qb[M];
int hha,tta,ans,cnt,hhb,ttb;
unordered_map<string,int> da;
unordered_map<string,int> db;

int extend(unordered_map<string,int> &da, unordered_map<string,int> &db, string a[],string b[],string *q,int &hh,int &tt)
{
    string x=q[hh++];
    for(int i=0;i<cnt;i++)
    {
        string rulea=a[i],ruleb=b[i];
        for(int j=0;j<x.size();j++)
        {
            string s2=x.substr(j,rulea.size());
            string state;
            if(s2==rulea){
                state=x.substr(0,j)+ruleb+x.substr(j+rulea.size());
                if(db.count(state))
                {
                    return da[x]+db[state]+1;
                }
                if(da.count(state))continue;
                da[state]=da[x]+1;
                q[++tt]=state;
            }
            
        }
    }
    return 11;
}

int bfs()
{
    if(A==B)return 0;
    qa[0]=A,qb[0]=B;
    da[A]=db[B]=0;
    while(hha<=tta&&hhb<=ttb)
    {
        int t;
        if(tta-hha<=ttb-hhb)t=extend(da,db,rule_a,rule_b,qa,hha,tta);
        else t=extend(db,da,rule_b,rule_a,qb,hhb,ttb);
        if(t<=10)return t;
    }
    return 11;
}
int main()
{
    cin>>A>>B;
    while(cin>>rule_a[cnt]>>rule_b[cnt])cnt++;
    int res=bfs();
    if(res>=11)cout<<"NO ANSWER!"<<endl;
    else cout<<res;
}


版本二:stl队列版本(懒人版本)!

//最小步数模型,每一步分支太多,若直接bfs会超时。故使用双端bfs
//双端bfs的理解:假设某次遍历后,从起点延伸的点集和从终点延伸的点集没有会师,则说明不存在长度小于等于d[a]max+d[b]max的路线,以此类推,第一次得到的一定是最短路
//技巧:距离数组和判重数组可以“合二为一”
//从队长较短的那一侧进行当一轮的延申,可以最小化“浪费”的状态,或者更直观地说可以使状态数量曲线(指数型)更加平滑。
#include <iostream>
#include <queue>
#include <map>
#include <algorithm>
using namespace std;

const int N=7,M=5e3+10;
string rule_a[N],rule_b[N],A,B;
int hha,tta,ans,cnt,hhb,ttb;
queue<string> qa,qb;
unordered_map<string,int> da;
unordered_map<string,int> db;

int extend(unordered_map<string,int> &da, unordered_map<string,int> &db, string a[],string b[],queue<string> &q)
{
    string x=q.front(); q.pop();
    for(int i=0;i<cnt;i++)
    {
        string rulea=a[i],ruleb=b[i];
        for(int j=0;j<x.size();j++)
        {
            //
            if(x.substr(j,rulea.size())==rulea){
                string state=x.substr(0,j)+ruleb+x.substr(j+rulea.size());
                if(db.count(state))
                {
                    return da[x]+db[state]+1;
                }
                if(da.count(state))continue;
                da[state]=da[x]+1;
                q.push(state);
            }
            
        }
    }
    return 11;
}

int bfs()
{
    if(A==B)return 0;
    qa.push(A),qb.push(B);
    da[A]=db[B]=0;
    while(qa.size()&&qb.size())
    {
        int t;
        if(qa.size()<=qb.size())t=extend(da,db,rule_a,rule_b,qa);
        else t=extend(db,da,rule_b,rule_a,qb);
        if(t<=10)return t;
    }
    return 11;
}
int main()
{
    cin>>A>>B;
    while(cin>>rule_a[cnt]>>rule_b[cnt])cnt++;
    int res=bfs();
    if(res>=11)cout<<"NO ANSWER!"<<endl;
    else cout<<res;
}


可以惊讶地发现:法一比法二慢了近五分之一!主要原因是,计算机访问一片内存会消耗时间,且耗时与内存大小正相关(具体的原理不太清楚,我是大二萌新),法一开了个可能有冗余的数组,那每一次访问数组元素都要耗时。而法二是用多少开多少,可以有效缓解这个问题

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值