POJ 2449 A* + spfa求第k短路

今天学习了 A* 算法,有了诸多感悟。

在求最优路径时,队列中有多个点我们可以用来拓展,换一种说法就是在当前阶段我们有多种走法,那么问题来了哪一种走法更加好呢?A* 算法实际上就是给我们一个标准来评估路径的好坏。现在给出 A* 的结论,当前最好的选择就是选择初始状态到当前状态的代价 + 当前状态到目标状态所需代价的估计值 所得到的值最小的点开始拓展。其实这个结论也是显而易见的。当前我们到底选择哪个点拓展呢?当然是选择使我们探索的总代价最小的点来拓展啦。如果我们不使用 A*,那么我们就要从队首点开始拓展了,完全是最笨的,明明有更好的选择你不去选择,为何非要多费力气呢?

公式表示为: f(n) = g(n) + h(n),
其中:
f(n) 是从初始状态经由状态n到目标状态的代价估计,
g(n) 是在状态空间中从初始状态到状态n的实际代价,
h(n) 是从状态n到目标状态的估计代价。
 
g(n) 是我们已经知道的,关键就是 h(n) 的选择了。既然是我们估计的代价,肯定是有偏差的吧。那么偏差导致的结果是什么?
我们以 d(n) 表示状态 n 到目标状态的最短距离,那么 h(n) 的选取有如下三种情况:
1.如果h(n) < d(n),这种情况下,搜索的点数多,搜索范围大,效率低。但能得到最优解。
2.如果h(n) = d(n),即距离估计 h(n) 等于最短距离,那么搜索将严格沿着最短路径进行, 此时的搜索效率是最高的。
3.如果h(n) > d(n),搜索的点数少,搜索范围小,效率高,但不能保证得到最优解。

在知道了上面的内容后,我们可以认为,bfs 是 f(n) 为 0 的情况,因为没有经过什么选择,每次都是从队首节点开始拓展。所以bfs是最烂的A*

 

再来说说 dijkstra。dijkstra 其实也是 A* 算法,他的估价函数是 f(n) = g(n) 即每一次都是取出队列中到源点最小距离的点来拓展。

我们可以试着理解一下,为什么这样的估价函数是对的。

求一个点到源点的最短距离,无非就是用他周围点的距离 + 权值来更新这个点的距离,取中间最小的一个。所以现在我们可以利用 bfs 设计出一个算法,源点进队,然后队首出队,利用队首点到源点的距离更新周围的点的距离,更新了就进队,就这样不断地进队出队更新,最后队列为空的时候也就代表着我们无法取出点来更新了,即所有点的距离都已经是最短距离。

这个就是最普通的 bfs,估价函数为 0,每次选择队首节点来更新

但是 dijkstra 非常聪明,他想到算一个点到源点的最短距离,在用一点来更新周围的点时,如果不是那种仅仅一次就更新出最短距离的最优情况,那么队列中肯定有一个点的多个距离(1),我们用哪个距离更新呢?肯定是选择这个点最小的距离更新了,较大的距离肯定没有较小的距离更新的好。所以我们可以使用优先队列来达到这个目的,每次取出到源点最小距离的点来更新。此外这个优先队列还有一个作用,在取出一个点的时候,能够保证距离比它小的点都已经拓展过了。也就是保证了,第一次取出来的时候就是最小距离。

求最短路径我们知道怎么算了,求k短路径怎么算呢?其实很简单,改进一下 dijkstra 就行了,从一个点拓展到另外一个点的时候,我们不去更新新得到的点,而是算出它的距离,然后进队。那么这个队列在我们整个计算的过程中肯定包含一个点到源点的所有可能的距离,因为我们用的是优先队列,所以第一次出来的是最短距离,第二次出来的就是次短距离,第 k 次出队的就是 k 短距离。

这是基本思路,不过计算量确实有点大。如果想要进一步优化的话,我们可以让估价函数变成 f(n) = g(n) + h(n)。h(n) 是从状态 n 到目标状态的估计代价。如果我们估计的话,估计的值必须得<= 实际值。幸运的是我们可以得到h(n)的实际值。很简单,预处理一下就行了,从终点反向 spfa,或者 dijstra,求出各点到终点的最小距离。这个不就是我们想要的h(n)吗?

所以现在的算法改进成了每次取出 g(n) + h(n) 最小的点去拓展,等到终点第 k 次出队的时候,就可以终止了。

下面是 POJ 2449 的代码:

#include <iostream>
#include <cstdlib>
#include <utility>
#include <cstring>
#include <cstdio>
#include <vector>
#include <queue>
#define INF 0x3f3f3f
#define maxn 1010
using namespace std;

typedef pair<int, int> pii;
struct edge{int to, cost;};
vector<edge> G[maxn];//正向图,邻接表
vector<edge> rG[maxn];//反向图,邻接表
int s, t, k;
int n, m, ok;

int d[maxn], In_que[maxn];//反向spfa
void spfa(int s){
    memset(In_que, 0, sizeof(In_que));
    memset(d, INF, sizeof(d));
    queue<int> Q;
    Q.push(s);
    d[s] = 0;
    In_que[s] = 1;
    while (!Q.empty()){
        int u = Q.front();
        Q.pop();
        In_que[u] = 0;
        for (int i = 0; i < rG[u].size(); i++){
            edge e = rG[u][i];
            int v = e.to, dd = d[u] + e.cost;
            if (d[v] > dd){
                d[v] = dd;
                if (!In_que[v]){
                    Q.push(v);
                    In_que[v] = 1;
                }
            }
        }
    }
}

struct state{
    int u, g, h;
    bool operator < (const state &b) const{
        return g + h > b.g + b.h;
    }
    state(int a, int b, int c):u(a), g(b), h(c){}
};
int cnt[maxn], kd[maxn];
void Astar(){
    memset(cnt, 0, sizeof(cnt));
    priority_queue<state> Q;//open表
    Q.push(state(s, 0, d[s]));
    while (!Q.empty()){
        state cur = Q.top();
        Q.pop();
        int u = cur.u;
        cnt[u]++;
        if (cnt[u] == k && u == t){//终点第k次出队
            printf("%d\n", cur.g);
            ok = 1;
            break;
        }
        if (cnt[u] > k)//如果出队次数大于k,不用拓展了,因为一个点的第k短距离肯定是由周围点的
            //前k短距离更新得来
            continue;
        for (int i = 0; i < G[u].size(); i++){
            edge e = G[u][i];
            if (cnt[e.to] != k){//如果这个点不在closed表中
                Q.push(state(e.to, cur.g + e.cost, d[e.to]));
            }
        }
    }
}

int main()
{
    //freopen("1.txt", "r", stdin);
    scanf("%d%d", &n, &m);
    for (int i = 0; i < m; i++){
        int u, v, c;
        scanf("%d%d%d", &u, &v, &c);
        G[u].push_back((edge){v, c});
        rG[v].push_back((edge){u, c});//反向建边
    }
    scanf("%d%d%d", &s, &t, &k);
    if (s == t) k++;//题目特殊要求。。
    spfa(t);
    Astar();
    if (!ok)//如果没有第k短路
        printf("-1\n");
    return 0;
}


(1)这就是为什么dijkstra中有一句 if(vis[i]) continue;的原因

hint:第一种bfs常常在拓展到一个新的点时候,就标记成true,第二种通常是出队的时候再标记true。 

 

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值