单源最短路径算法详解 Bellman-Fold,SPFA,Dijkstra

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_35558510/article/details/88936364

 

松弛操作

Bellman-Fold算法

SPFA (Shortest Path Faster Algorithm)算法

Dijkstra算法(使用最小堆优化选未收敛估距离最小点操作)


松弛操作

对一条有向边(u,v)进行松弛操作就是。看走边 (u, v) 到 v 能否更近。

if (dist[u] + w(u, v) < dist[v]){
    dist[v] = dist[u] + w(u, v);
}

Bellman-Fold算法

 

每个点有一个最短距离估计值 dist 属性,其初值:源点为0,其他点为无穷大。

算法的操作核心就是维护 dist 数组,不停的迭代式修改。

 

N个节点,M条边的有向图,任何一个点到源点的最短路径的长度不超过N-1。

 

经过第一遍对所有的松弛操作,最短路径长度为1的点的最短距离估计值已等于最短距离值,后续不会再有变化。

 

由于长度为 k 的一条最短路径一定是由一条长度为 k-1 的一条最短路径再加一边组成的。因此,经过第二遍对所有边的松弛操作,最短路径长度为2的点的最短距离估计值已等于最短距离值,后续不会再有变化。

 

......

 

因此,经过第N-1遍对所有边的松弛操作,最短路径长度为N-1的点的最短距离估计值已等于最短距离值,后续不会再有变化。由于任何一个点到源点的最短路径的长度不超过N-1,现在图中所有点的最短路径估计值均为最短距离值。

 

并且,只要在遍历过程中,在松弛操作更新最短距离估计值时,同时更新其前序数组,那么最短路径也记录下来了。

 

Bellman-Fold算法还可以判断图中是否存在源点可达的负权值环路。其操作就是再进行一遍(也就是第 N 遍)对所有边的松弛操作,如果出现了距离更新,那么就出现了负权值环路。

 

其本质就是负权值环路会导致距离估计值在 n-1 次全局松弛内必然不会收敛。

void BellmanFord() {
    for(int i=1;i<=N;i++) dist[i] = INT_MAX;
    dist[S]=0;

    for (int i = 0; i < N - 1; i++) {
        for (int j = 0; j < M; j++) { //遍历每一条边
            if (dist[from[j]] + weight[j] < dist[to[j]]) { 
                dist[to[j]] = dist[from[j]] + weight[j];
            }
        }
    }
}

事实上我们在算法实现时,很难保证每一轮的变化在整个一轮结束时才应用变化。在每遍历一条边的时候,我们都是立刻应用变化。然后,会出现链式修改。不过这只会让算法在前面的循环收敛的更快而已,并不会影响算法的正确性。

SPFA (Shortest Path Faster Algorithm)算法

Bellman-Fold算法有一个严重的缺点(以至于这个算法本身处理提供一种朴素的思想外没有任何的使用价值),他是纯粹的盲目的循环松弛所有的边。然而,并不是在每一次循环中,每一条边都有必要进行松弛

 

可以发现,如果某个点这轮的 dist 都没变,那么下一轮再松弛由它发出的边是无意义的。我们便以此对Bellman Fork算法对所有边的循环进行精简。

 

我们借鉴 BFS 的思路,使用一个队列来维护真正需要进行松弛的边的起点集合

 

我们初始时将源点加入队列。 然后每次从队列中取出一个元素,并对所有由它发出的边进行松弛。若松弛成功,则在将这条边所指向的相邻点入队

 

如果循环到对列为空,那么说明此时所有的距离估计值已经收敛。

 

显然,这个算法不再需要循环 n-1 次这个很粗略(因为只有近乎线性的网络才会出现到一个点的最短路径需要遍历所有节点,因此大部分网络只需要循环远远不到 n-1 次即可收敛)的收敛条件,故此算法的效率要远高于Bellman-Fold算法。

bool isInQueue[MAXN];
void SPFA() {
    for (int i = 1; i <= N; i++) dist[i] = INT_MAX;
    dist[S] = 0;
    /*其实就是BFS的架子*/
    queue<int> que;
    que.push(S);
    
    while (not que.empty()) {
        int u = que.front(); que.pop(); isInQueue[u] = false;
        for (int i = head[u]; i; i = next[i]) {
            int v = to[i];
            if (dist[u] + weight[i] < dist[v]) {
                dist[v] = dist[u] + weight[i];
                if (not isInQueue[v]) {
                    que.push(v); isInQueue[v] = true;
                }
            }
        }
    }
}

Dijkstra算法(使用最小堆优化选未收敛估距离最小点操作)

虽然SPFA 不再盲目的所有边进行松弛了,而是对松弛后可能导致 dist 值变化的边进行松弛。但是其还具有一定的盲目性!!!因为松弛边的起点的 dist 如果不已是最短路径,那么在松弛边的起点的dist一定会再发生改变,那么同样的边还会再进行若干次松弛操作。因此,我们想只有松弛的边的起点的dist已经是最短路径时,才对此边进行松弛操作。这样,任何边最多只会进行一次松弛操作。这样效率就提上去了!!!

 

因此,我们很自然的会想到维护一个逻辑上的已收敛节点集合和未收敛点集合。

 

每次从未收敛点集合中找到 dist 最小点加入已收敛点集并对其发出的边进行松弛,此时这个点的 dist 值就是最短距离,直至全部的点加入了收敛点集合。

 

那么为什么从未收敛点集合中 dist 最小点就是收敛的呢?我们假设此时的 dist 不是最小值,也就真实最短路径比他还小。那么最后一跳必然不在已收敛集合中(所有的收敛点都松弛过其周围的边了)。那么最后一跳一定在未收敛集合内。那么最后一条的前某一跳一定是从已收敛点集合跳到未收敛点集合。那么其完整路径的长度必然比从未收敛点集合中 dist 最小值然而我们假定了真实的 dist 比 从未收敛点集合中 dist 最小值互相矛盾。

 

那么我们如何快速的从未收敛点集合中找到 dist 最小点呢?使用最小堆存放未收敛点距离估计值集合即可。

struct Node { //堆节点
    int u;//节点号
    int w;//权值
    //优先队列默认是最大堆,因此我们将比较运算结果反向即可变为最小堆
    bool operator<(const Node& node) const {
        return w > node.w;
    }
};

void Dijkstra() {
    for (int i = 1; i <= N; i++) dist[i] = INT_MAX;
    dist[S] = 0;
    priority_queue<Node> que; //堆
    que.push((Node) {S, 0});

    while (not que.empty()) {
        int u = que.top().u;
        int w = que.top().w;
        que.pop();
        /*如果权值不是最新,说明已经有新的同样的点加入队列,这是无效的旧值。
        **使用了惰性删除的思想,避免了主动修改堆中数据的值这个难以实现的操作
        */
        if (w != dist[u]) {
            continue;
        }
        for (int i = head[u]; i; i = next[i]) {
            int v = to[i];
            if (dist[u] + weight[i] < dist[v]) {
                dist[v] = dist[u] + weight[i];
                que.push((Node) {v, dist[v]});
            }
        }
    }
}

惰性删除就是,如果在某时判断出了要删除某一点,可以不立刻删除,而是在后续的某个时刻要用它的时候,发现他是不合法的,忽视他。

猜你喜欢

转载自blog.csdn.net/qq_35558510/article/details/88936364
今日推荐