BFS——双向广搜+A—star

有时候从一个点能扩展出来的情况很多,这样几层之后搜索空间就很大了,我们采用从两端同时进行搜索的策略,压缩搜索空间。

  1. 字串变换(190. 字串变换 - AcWing题库)

思路:这题因为变化规则很多,所以我们一层一层往外扩展的时候,扩展几层后空间就会变得很大 ,那么就需要换一个思路,我们这里采用双向广搜,从两个方向来进行搜索,具体执行的时候,肯定得先从一个方向开始,那么从哪里开始呢?显然要从状态少的方向开始,我们就比较两个队列,从小的那个队列开始,然后又有新问题了,如果从一个方向开始,什么时候判停去找下一个方向,显然我们可以只往外扩展一层,扩展结束,或者两者头尾交汇的时候停止。

现在理一下我们需要哪些数据结构,首先得有两个队列分别记录从头搜索和从尾搜索的队列,另外要有两个数据结构来记录每个状态从头和从尾更新到它们时的距离。另外还要记录一下变换规则。

还有就是我们每次只往外扩展一层,所以有交汇和不交汇两种情况,如果交汇的话,那么我们可以直接返回从头到这个点和从尾到这个点的步数和,如果这个值小于10,那么显然就是最小距离,因为我们每次只从头或者从尾往外扩展一层,下次扩展时找到的,一定比当前扩展找到的多一步。当然也可能不交汇,因为我们只往外扩展一层,不交汇的时候返回一个大于10的数即可。同时我们每一次往外扩展都要记录步数,当步数大于10的时候直接停下。因为题目要求的上限就是10步。

对了如果想把两个扩展函数写在一起,一定要传入它们的更新规则,两者的扩展规则是不一样的。

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
string a[10],b[10];
int n;
int expend(queue<string>&q,unordered_map<string,int>&dl,unordered_map<string,int>&dr,string a[],string b[])
{
    int d=dl[q.front()];
    while(q.size()&&dl[q.front()]==d)
    {
        auto t=q.front();
        //cout<<t<<endl;
        q.pop();
        for(int i=0;i<n;i++)
        {
            for(int j=0;j<t.size();j++)
            {
                if(t.substr(j,a[i].size())==a[i])
                {
                    string tmp=t.substr(0,j)+b[i]+t.substr(j+a[i].size());
                    if(dr.count(tmp)) return dl[t]+dr[tmp]+1;
                    if(dl.count(tmp)) continue;
                    q.push(tmp);
                    dl[tmp]=dl[t]+1;
                }
            }
        }
    }
    return 11;
}
int bfs(string s,string e)
{
    if(s==e) return 0;
    queue<string>ql,qr;
    unordered_map<string,int>dl,dr;
    ql.push(s),qr.push(e);
    dl[s]=dr[e]=0;
    int step=0;
    while(ql.size()&&qr.size())
    {
        int t;
        if(ql.size()<qr.size()) t=expend(ql,dl,dr,a,b);
        else t=expend(qr,dr,dl,b,a);
        if(t<=10) return t;
        if(++step>=10) return -1;
    }
    return -1;
}
int main()
{
    string s,e;
    cin>>s>>e;
    n=0;
    while(cin>>a[n]>>b[n])n++;
    int ans=bfs(s,e);
    if(ans==-1) cout<<"NO ANSWER!";
    else cout<<ans;
}

另外这里补充一下substr()的用法:

形式 : s.substr(pos, len)

返回值: string,包含s中从pos开始的len个字符的拷贝(pos的默认值是0,len的默认值是s.size() - pos,即不加参数会默认拷贝整个s)

异常 :若pos的值超过了string的大小,则substr函数会抛出一个out_of_range异常;若pos+n的值超过了string的大小,则substr会调整n的值,只拷贝到string的末尾

参考链接:C++中substr()函数用法详解_substr c++-CSDN博客

A---star算法

A---star算法也是对搜索空间进行压缩进而优化搜索。这里我们选择下一个搜索位置的时候,并不是根据它们到起点的位置来选择的,而是根据它们到起点的位置+到终点的预估值来进行选择,可以这么来理解,我们之前的bfs都是假设到终点的预估值为0,那么直接放入队列即可,这里需要用到优先队列,将从起点搜到的实际距离+到终点预估距离作为权重,放入优先队列。而预估距离要满足的条件就是小于真实距离,而且要是答案有解的情况下才能使用这个算法,否则不如bfs。

而且可以证明终点第一次出队的时候是得到的距离是最小距离。

参考链接:

AcWing 178. 第K短路(A* 反向计算最短路作为到终点的估计值) - AcWing

  1. 第K短路(178. 第K短路 - AcWing题库

思路:这道题要求起点到终点的第k短路,我们已知终点第一次出队的时候是最短路,那么第二次出队就是第儿短路,因为这条路径是被其他点更新了放进队列的,以此类推,我们只要获得终点第k次出队时的距离则可以得到答案。

然后问题就是这里的预估距离该怎么处理,因为终点固定,所以我们可以用dijkstra算法计算各点到终点的距离作为预估距离。因为它满足小于等于真实距离的条件,所以符合作为预估距离的要求。

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
const int N=1010,M=20010;
typedef pair<int,int> pii;
typedef pair<int,pii> piii;
int n,m;
int rh[N],h[N],e[M],ne[M],w[M],idx;
int s,t,k;
int d[N],st[N];
void add(int h[],int a,int b,int c)
{
    w[idx]=c,e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}
void dijkstra()
{
    memset(d,0x3f,sizeof d);
    d[t]=0;
    priority_queue<pii, vector<pii>, greater<pii>> q;
    q.push({0,t});
    while(q.size())
    {
        auto it=q.top();
        q.pop();
        int dist=it.first,v=it.second;
        if(st[v]) continue;
        st[v]=1;
        for(int i=rh[v];i!=-1;i=ne[i])
        {
            int j=e[i];
            if(d[j]>dist+w[i])
            {
                d[j]=dist+w[i];
                q.push({d[j],j});
            }
        }
    }
}
int cnt[N];
int bfs()
{
    priority_queue<piii, vector<piii>, greater<piii>> q;
    q.push({d[s],{0,s}});
    while(q.size())
    {
        auto it=q.top();
        q.pop();
        int dist=it.second.first,v=it.second.second;
        cnt[v]++;
        if(v==t&&cnt[v]==k) return dist;
        for(int i=h[v];i!=-1;i=ne[i])
        {
            int j=e[i];
            if(cnt[j]<k)
            q.push({dist+w[i]+d[j],{dist+w[i],j}});
        }
    }
    return -1;
}
int main()
{
    memset(h,-1,sizeof h);
    memset(rh,-1,sizeof rh);
    scanf("%d%d",&n,&m);
    for(int i=1;i<=m;i++)
    {
        int a,b,c;
        scanf("%d%d%d",&a,&b,&c);
        add(h,a,b,c);
        add(rh,b,a,c);
    }
    scanf("%d%d%d",&s,&t,&k);
    if(s==t) k++;//因为最初s就会被放进去,每条最短路中至少要包含一条边。
    dijkstra();
    cout<<bfs();
}
  1. 八数码(179. 八数码 - AcWing题库

这题是将棋盘整个视为一种状态,我们每一个点最多只能扩展出四种状态,我们可以用之前魔板那道题的写法,稍微变一下,变成双向广搜,自然也可以用A---star算法,因为状态还是有些许多。

A---star算法需要保证有解,那么我们就提前预判一下是否有解,如果有解再去进行查找。

八数码问题有个预判的技巧,我们按行读入数字,得到的一个数组中,如果逆序对的数量为奇数,那么就无解,如果逆序对的数量为偶数,那么就有解。

可以这么来理解,如果我们在行内进行移动,实际上并没有改变序列,也就是并未改变逆序对数量,如果在行与行之间进行移动,那么相当于只改变了它前面或者后面两个数的位置,我们以一种情况为例,它实际上就只改变了3个数内部的相对顺序,所以实际上逆序对的数量要么不变要么就多2或者少2,所以起始状态和结束状态中逆序对的奇偶性相同,我们可以顺序排列的时候逆序对的数量是0,那么起始状态中逆序对的数量应该是偶数。

然后就是考虑估价函数,我们计算出每个数当前位置和目标位置的曼哈顿距离,很显然,每次移动最好的情况只会让一个数和它的实际位置的曼哈顿距离减1,所以我们可以将每个状态中每个数当前位置与目标位置的曼哈顿距离和算出来,作为预估距离。

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
string s,e;
char a[4][4];
unordered_map<string,int>d;
unordered_map<string,pair<string,char>>pre;
char op[]={'u','d','l','r'};//x的位置
void toa(string t)
{
    for(int i=0;i<t.size();i++)
    {
        a[i/3][i%3]=t[i];
    }
}
string tos()
{
    string res="";
    for(int i=0;i<3;i++)
        for(int j=0;j<3;j++)
            res += a[i][j];
    return res;
}
string move0(string t)//u
{
    toa(t);
    int x,y;
    for(int i=0;i<3;i++)
        for(int j=0;j<3;j++)
            if(a[i][j]=='x') 
                x=i,y=j;
    if(x!=0)
    {
        swap(a[x][y],a[x-1][y]);
    }
    return tos();
}
string move1(string t)//d
{
    toa(t);
    int x,y;
    for(int i=0;i<3;i++)
        for(int j=0;j<3;j++)
            if(a[i][j]=='x') 
                x=i,y=j;
    if(x!=2)
    {
        swap(a[x][y],a[x+1][y]);
    }
    return tos();
}
string move2(string t)//l
{
    toa(t);
    int x,y;
    for(int i=0;i<3;i++)
        for(int j=0;j<3;j++)
            if(a[i][j]=='x') 
                x=i,y=j;
    if(y!=0)
    {
        swap(a[x][y],a[x][y-1]);
    }
    return tos();
}
string move3(string t)//r
{
    toa(t);
    int x,y;
    for(int i=0;i<3;i++)
        for(int j=0;j<3;j++)
            if(a[i][j]=='x') 
                x=i,y=j;
    if(y!=2)
    {
        swap(a[x][y],a[x][y+1]);
    }
    return tos();
}
typedef pair<int,pair<int,string>> piis;
int tj(string g)
{
    int cnt=0;//逆序对数量
    for(int i=0;i<g.size();i++)
    {
        for(int j=i+1;j<g.size();j++)
        {
            if(g[i]>g[j]) cnt++;
        }
    }
    return cnt;
}
void bfs()
{
    priority_queue<piis,vector<piis>,greater<piis>>q;
    int cnt=tj(s);
    q.push({cnt,{0,s}});
    d[s]=0;
    if(s==e) return;
    while(q.size())
    {
        auto t=q.top();
        q.pop();
        string tv=t.second.second;
        int dist=t.second.first;
       // cout<<tv<<endl;
        string tmp[5];
        tmp[0]=move0(tv);
        tmp[1]=move1(tv);
        tmp[2]=move2(tv);
        tmp[3]=move3(tv);
        for(int i=0;i<4;i++)
        {
           // cout<<tmp[i]<<endl;
            if(d.count(tmp[i])) continue;
            d[tmp[i]]=dist+1;
            pre[tmp[i]]={tv,op[i]};
            cnt=tj(tmp[i]);
            q.push({cnt+d[tmp[i]],{d[tmp[i]],tmp[i]}});
            if(tmp[i]==e) return;
        }
        //cout<<endl;
    }
}
int main()
{
    string g="";
    char c;
    while(cin>>c)//不会录入空格
    {
        s += c;
        if(c!='x') g+=c;
    }
    e="12345678x";
    int cnt=0;//逆序对数量
    for(int i=0;i<g.size();i++)
    {
        for(int j=i+1;j<g.size();j++)
        {
            if(g[i]>g[j]) cnt++;
        }
    }
    if(cnt%2) cout<<"unsolvable";
    else
    {
        bfs();
        //cout<<d[e]<<endl;
        if(d[e])
        {
            string res="";
            while(e!=s)
            {
                res += pre[e].second;
                e=pre[e].first;
            }
            reverse(res.begin(),res.end());
            cout<<res;
        }
    }
}

ps:代码看似复杂,但复用率极高,只要把逻辑盘清楚,实际上并不麻烦。

总的来说,A---star可以提高效率,但是估价函数一定要写明白。

另外补充一点,cin录单个字符的时候不会把空格录进去,如果没有更好的方法避免空格的话,可以用cin。

相关推荐
chenziang12 小时前
leetcode hot100 环形链表2
算法·leetcode·链表
Captain823Jack3 小时前
nlp新词发现——浅析 TF·IDF
人工智能·python·深度学习·神经网络·算法·自然语言处理
Captain823Jack4 小时前
w04_nlp大模型训练·中文分词
人工智能·python·深度学习·神经网络·算法·自然语言处理·中文分词
是小胡嘛4 小时前
数据结构之旅:红黑树如何驱动 Set 和 Map
数据结构·算法
m0_748255024 小时前
前端常用算法集合
前端·算法
呆呆的猫5 小时前
【LeetCode】227、基本计算器 II
算法·leetcode·职场和发展
Tisfy5 小时前
LeetCode 1705.吃苹果的最大数目:贪心(优先队列) - 清晰题解
算法·leetcode·优先队列·贪心·
余额不足121385 小时前
C语言基础十六:枚举、c语言中文件的读写操作
linux·c语言·算法
火星机器人life7 小时前
基于ceres优化的3d激光雷达开源算法
算法·3d
虽千万人 吾往矣7 小时前
golang LeetCode 热题 100(动态规划)-更新中
算法·leetcode·动态规划