BFS A*优化模板 && 普通BFS、A*BFS判重对比

引入

A*算法主要思想是尽可能减少搜索空间;

我觉得A*算法和迪杰斯特拉算法真的很像;

具体看下面;

一般思路

将BFS中的队列换成优先队列,这里我们使用小根堆来实现;

队列中存放的值为起点到当前点的真实距离当前点到终点的估计距离

这个估计距离是由我们写的一个估价函数来算的;


为什么说A*和迪杰斯特拉很像;

因为迪杰斯特拉不就是把估计距离当作零吗?

保证正确性

g ( i ) g(i) g(i)表示点 i i i到终点的真实距离

f ( i ) f(i) f(i)表示点 i i i到终点的估计距离

只要保证 f ( i ) ≤ g ( i ) f(i)≤g(i) f(i)g(i)且估计距离为非负数,那么第一次从队头出来的答案必然是正确的;

当然越接近越好,如果预估到0,那不就是个迪杰斯特拉…

应用场景

需要保证答案有解

因为如果无解,那么我们会搜索完全部的状态;

那就退化成了普通的BFS;

但是我们又使用了优先队列( O ( l o g n ) O(logn) O(logn));

因此比普通的BFS(普通队列 O ( 1 ) ) O(1)) O(1))效率还低;


当然,如果无法判断是否有解;

我觉得可以试试A*,毕竟一般来说效率都比朴素的高;

注意点

A*算法只能保证当终点出队的时候距离是最小的;

但是不能保证中间某些点出队的时候距离最小;

因此每个点不止被访问一次(因为有可能先遍历到错的路径)

普通BFS、A*、迪杰斯特拉判重对比

普通BFS:一般在入队的时候判重;

堆优化的迪杰斯特拉:一般在出队的时候判重;

A*:类似spfa,不能判重;


例题

八数码

八数码
在这里插入图片描述
在这里插入图片描述

思路

我们发现方块左右移动是不改变逆序对的数量;

而上下移动改变逆序对的数量必然是偶数(可能是0);

因此如果初始状态逆序对的数量是奇数,那么必然无解;

如果是偶数,那么必然有解;


因为最终排列是 12345678 x 12345678x 12345678x

而我们只需要保证,预估距离小于等于真实距离

而每个点最终必然是按 12345678 x 12345678x 12345678x排列;

因此我们的估价函数就认为是当前状态到最终状态的曼哈顿距离;

曼哈顿距离如下;

在这里插入图片描述

Code

#include <iostream>
#include <cstdio>
#include <string>
#include <utility>
#include <queue>
#include <algorithm>
#include <unordered_map>

using namespace std;

typedef long long ll;

const int N = 1e5 + 10;

//估价函数
int f(string state){
    int ret = 0;
    for(int i=0;i<state.size();++i){
        if(state[i] != 'x'){
            //计算曼哈顿距离
            int tar = state[i] - '1';
            int nowx = i/3,nowy = i%3,tarx = tar/3,tary = tar%3;
            ret += abs(nowx-tarx) + abs(nowy-tary);
        }
    }
    return ret;
}

string over = "12345678x";

int dx[] = {0,0,1,-1};
int dy[] = {1,-1,0,0};//右左下上
string opt = "rldu";

typedef pair<int,string> pis;

priority_queue<pis,vector<pis>,greater<pis> >pq;

unordered_map<string,int> dist;//真实距离

unordered_map<string,pair<string,char> > pre;

string bfs(string start){
    if(start == over) return "";
    dist[start] = 0;
    pq.push({f(start),start});
    while(!pq.empty()){
        auto u = pq.top();
        pq.pop();
        string now = u.second;
        if(now == over) break;
        //先找到x
        int px,py;
        for(int i=0;i<now.size();++i){
            if(now[i] == 'x'){
                px = i/3,py = i%3;
                break;
            }
        }
        //将x上下左右移动
        for(int i=0;i<4;++i){
            string state = now;
            int xx = px + dx[i],yy = py + dy[i];
            if(xx < 0 || xx >= 3 || yy < 0 || yy >= 3){
                continue;
            }
            //移动过去
            swap(state[px*3+py],state[xx*3+yy]);
            if(!dist.count(state) || dist[state] > dist[now] + 1){
                dist[state] = dist[now] + 1;
                pre[state] = {now,opt[i]};
                //真实距离 + 预测距离
                pq.push({f(state)+dist[state],state});
            }
        }
    }
    string ret;
    while(over != start){
        ret += pre[over].second;
        over = pre[over].first;
    }
    reverse(ret.begin(),ret.end());
    return ret;
}

void solve(){
    string start,seq;//seq用于计算逆序对
    char ch;
    while(cin >> ch){
        start += ch;
        if(ch != 'x') seq += ch;
    }
    int num = 0;
    for(int i=1;i<8;++i){
        for(int j=i-1;j>=0;--j){
            if(seq[i] > seq[j]) ++num;
        }
    }
    if(num & 1) cout << "unsolvable\n";
    else cout << bfs(start) << '\n';
}

int main(){
    std::ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
    solve();
    return 0;
}

第K短路

第K短路

在这里插入图片描述
在这里插入图片描述

思路

因为A*算法能保证终点出队的时候必然是最优的;

那么当终点第 k k k次出队的时候,这条路必然就是 k k k短路

接着是考虑估价函数;

对于估价函数来说,我们只需要保证预估距离小于等于真实距离

那么我们只需要求终点到所有点的最短路即可;

也就是用一个迪杰斯特拉求单源最短路;

以最短路作为我们的估价函数,那必然是符合要求的;


因此我们只需要

  1. 由终点跑迪杰斯特拉单源最短路;
  2. 从起点跑A*算法求第 k k k短路;

注意

在这里插入图片描述
因此当起点和终点重合的时候,我们需要找第 k + 1 k+1 k+1短路;

因为最短路一条边也不包含;

Code

#include <iostream>
#include <cstdio>
#include <cstring>
#include <queue>
#include <vector>
#include <algorithm>

using namespace std;

typedef long long ll;

const int N = 1e5 + 10;

struct Edge{
    int to,next,val;
}e[N<<1];//需要正向边和反向边

int head[N],dist[N],vis[N],rhead[N],cnt;

void add(int head[],int u,int v,int w){
    e[++cnt].to = v;
    e[cnt].val = w;
    e[cnt].next = head[u];
    head[u] = cnt;
}

int n,m,start,over,k;

typedef pair<int,int> pii;//真实距离,编号

typedef pair<int,pii> pip;//真实距离+预估距离,真实距离,编号

void dijkstra(){
    memset(dist,0x3f,sizeof dist);
    priority_queue<pii,vector<pii>,greater<pii>> heap;
    dist[over] = 0;
    heap.push({0,over});
    while(!heap.empty()){
        auto u = heap.top();
        heap.pop();
        if(vis[u.second]) continue;
        vis[u.second] = 1;
        for(int i=rhead[u.second];i;i=e[i].next){
            int to = e[i].to;
            if(dist[to] > dist[u.second] + e[i].val){
                dist[to] = dist[u.second] + e[i].val;
                heap.push({dist[to],to});
            }
        }
    }
}
int f(int x){//估价函数
    return dist[x];
}

int a_star(){
    //判断无解的情况
    //也就是如果起点无法到终点
    //比如数据
    //3 2
    //1 2 1
    //2 1 1
    //1 3 1000 跑满k,但是不存在,硬跑TLE
    if(dist[start] == 0x3f3f3f3f) return -1;
    priority_queue<pip,vector<pip>,greater<pip>> heap;
    heap.push({f(start),{0,start}});
    int times = 0;
    while(!heap.empty()){
        auto u = heap.top();
        heap.pop();
        if(u.second.second == over) ++times;//记录第几次到终点
                        //返回真实距离
        if(times == k) return u.second.first;
        for(int i=head[u.second.second];i;i=e[i].next){
            int to = e[i].to;
            //边权 + 当前点已经包含的距离
            int real_dist = e[i].val+u.second.first;
            heap.push({real_dist+f(to),{real_dist,to}});
        }
    }
    return -1;
}

void solve(){
    cin >> n >> m;
    for(int i=1,u,v,w;i<=m;++i){
        cin >> u >> v >> w;
        add(head,u,v,w);
        add(rhead,v,u,w);
    }
    cin >> start >> over >> k;
    if(start == over) ++k;//第k+1短路
    dijkstra();
    cout << a_star() << '\n';
}

int main(){
    std::ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
    solve();
    return 0;
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值