由A*的学习引发对bfs, dijkstra的思考

来源:互联网 发布:算法工程师考试题目 编辑:程序博客网 时间:2024/04/29 05:20

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

以我现在的认识来看bfs有两种,第一种是最普通的bfs,即纯粹的层次遍历,第二种是有A*优化的bfs,常常用来求最优路径。

 

在求最优路径中,现在队列中有多个点我们可以用来拓展,换一种说法就是在当前阶段我们有多种走法,那么问题来了哪一种走法更加好呢?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),搜索的点数少,搜索范围小,效率高,但不能保证得到最优解

在知道了上面的内容后,如果open表不是用队列实现的话,我们也可以认为第一种bfs是f(n) = g(n) 的情况,因为是层次遍历,所以取出来的点到源点的距离肯定是逐渐增大的。在open表是用队列实现的情况下,我们又可以认为,普通的bfs是f(n)为0的情况,因为没有经过什么选择,每次都是从队首节点开始拓展,如果这样想的话,普通的bfs是最烂的A*

 

再来说说dijkstra。dijkstra就是第二种情况的bfs,他的估价函数是f(n) = g(n) 即每一次都是取出队列中到源点最小距离的点来拓展。

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

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

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

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

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

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

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

下面是poj2449的代码:

#include <iostream>#include <cstdlib>#include <utility>#include <cstring>#include <cstdio>#include <vector>#include <queue>#define INF 0x3f3f3f#define maxn 1010using 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];//反向spfavoid 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。 

 

0 0
原创粉丝点击