Johnson 全源最短路

(洛谷日报)

直接照搬了,部分删减

 

Johnson Floyd 一样,是一种能求出无负环图上任意两点间最短路径的算法。

1 算法概述


任意两点间的最短路可以通过枚举起点,跑 n Bellman-Ford 算法解决,时间复杂度是$O(n^2m) $的,也可以直接用 Floyd 算法解决,时间复杂度为 $ O(n^2m) $

注意到堆优化的 Dijkstra 算法求单源最短路径的时间复杂度比 Bellman-Ford 更优,如果枚举起点,跑nDijkstra算法,就可以在(本文中的Dijkstra采用 priority_queue 实现,下同)的时间复杂度内解决本问题,比上述跑 $n$ Bellman-Ford 算法的时间复杂度更优秀,在稀疏图上也比 Floyd 算法的时间复杂度更加优秀。

Dijkstra 算法不能正确求解带负权边的最短路,因此我们需要对原图上的边进行预处理,确保所有边的边权均非负。

Johnson 算法则通过另外一种方法来给每条边重新标注边权。

我们新建一个虚拟节点(在这里我们就设它的编号为 0 )。从这个点向其他所有点连一条边权为 0 的边。

接下来用 Bellman-Ford 算法求出从0号点到其他所有点的最短路,记为 

假如存在一条u点到v点,边权为w的边,则我们将该边的边权重新设置为

接下来以每个点为起点,跑 nDijkstra 算法即可求出任意两点间的最短路了。

容易看出,该算法的时间复杂度是 

Q:那这么说,Dijkstra 也可以求出负权图(无负环)的单源最短路径了?
A:没错。但是预处理要跑一遍 Bellman-Ford,还不如直接用 Bellman-Ford 呢。

2 正确性证明


为什么这样重新标注边权的方式是正确的呢?

在讨论这个问题之前,我们先讨论一个物理概念——势能。

诸如重力势能,电势能这样的势能都有一个特点,势能的变化量只和起点和终点的相对位置有关,而与起点到终点所走的路径无关。

势能还有一个特点,势能的绝对值往往取决于设置的零势能点,但无论将零势能点设置在哪里,两点间势能的差值是一定的。

接下来回到正题。

在重新标记后的图上,s点到t点的一条路径的长度表达式如下:

化简后得到:

无论我们从 s t走的是哪一条路径,的值是不变的,这正与势能的性质相吻合!

为了方便,下面我们就把$h_i$称为$i$点的势能。

上面的新图中s$\to$t的最短路的长度表达式由两部分组成,前面的边权和为原图中s→t 的最短路,后面则是两点间的势能差。因为两点间势能的差为定值,因此原图上s$\to$t 的最短路与新图上s$\to$t的最短路相对应。

到这里我们的正确性证明已经解决了一半——我们证明了重新标注边权后图上的最短路径仍然是原来的最短路径。接下来我们需要证明新图中所有边的边权非负,因为在非负权图上,Dijkstra 算法能够保证得出正确的结果。

根据三角形不等式,新图上任意一边 (u,v)上两点满足:。这条边重新标记后的边权为 。这样我们证明了新图上的边权均非负。

至此,我们就证明了 Johnson 算法的正确性。

3 代码


洛谷板子题

//Johnson全源
#include<iostream>
#include<cstdio>
#include<queue>
#include<cstring>
#define NUM 3010
#define INF 1e9
//9个0,1e9
#define FOR(a,b,c) for( int a = b;a <= c;a++ )
using namespace std;

int n,m;    
struct bian{
    int next,to;
    long long w;
};
bian e[NUM<<3];
struct dian{
    int id,dis;//点的编号,该点到目标点的距离
    bool operator < (const dian &x) const{
        return dis > x.dis;//小根堆
    }
    dian( int x,int y ){
        dis = x;id = y;//先距离后编号
    }
};
int head[NUM];
int t[NUM];//被更新的次数
long long h[NUM];
long long d[NUM];//点的距离
bool v[NUM];
int cnt;

void add( int x,int y,long long w ){
    e[++cnt].next = head[x];
    e[cnt].to = y;
    e[cnt].w = w;
    head[x] = cnt;
}
bool spfa(){ //spfa求个单源最短路
    queue <int> q;
    memset( h,63,sizeof(h) );//设置为最大值,玄学memset
    q.push(0);
    h[0] = 0;v[0] = 1;
    while( !q.empty() ){
        int hao = q.front();//这个边的起点
        q.pop();v[hao] = 0;//出队
        for( int i = head[hao];i;i = e[i].next ){
            int to = e[i].to;
            
            if( h[to] <= h[hao] + e[i].w ) continue;//无需更新
            
            h[to] = h[hao] + e[i].w;//更新路程
            if( v[to] ) continue;//已经在栈里了,就不管了
            q.push(to);v[to] = 1;//入栈
            t[to]++;//更新,为了判断负环
            if( t[to] >= n+1 ) return 1;//存在负环
        }
    }
    return 0;
}
void dij( int s ){ //普通的迪杰堆优化
    priority_queue <dian> q;
    FOR( i,1,n ) //初始化
        d[i] = INF;
    memset( v,0,sizeof(v) );//重复使用一下v数组
    
    d[s] = 0;
    q.push( dian(0,s) );//放入起点
    
    while( !q.empty() ){
    
        int hao = q.top().id;//最近的点的编号
        q.pop();
        if( v[hao] ) continue;
        v[hao] = 1;//这个点已经作为了更新点
        
        for( int i = head[hao];i;i = e[i].next ){
            int to = e[i].to;
            if( d[to] > d[hao] + e[i].w ){
                d[to] = d[hao] + e[i].w;
                if( v[to] ) continue;//放到下面来!
                q.push( dian( d[to],to ) );//放入终点
            }
        }
        
    }
}
int main(){
    
    cin >> n >> m;
    FOR( i,1,m ){
        int x,y,w;
        cin >> x >> y >> w;
        add( x,y,w );
    }
    FOR( i,1,n ) add( 0,i,0 );
    
    if( spfa() ){
        cout << -1;
        return 0;
    }

    FOR( x,1,n )
        for( int i = head[x];i;i = e[i].next )
            e[i].w += h[x] - h[e[i].to];//令边权都非负
    
    FOR( i,1,n ){
        dij( i );
        long long ans = 0;
        FOR( j,1,n ){
            if( i == j ) continue;
            if( d[j] == INF )
                ans += j * INF;
            else 
                ans += j * ( d[j] + h[j] - h[i] );
        }
        cout << ans << endl;
    }
    
    
    return 0;
}
Johnson全源最短路板子

 

4 第一次敲板子出现的问题


1. 没输入m,边是按照n输入的....

2. 没在dij取堆顶的时候判断这个点是不是已经被访问过了

3. 学了个构造函数(也许?)

4. 没开long long

5. 边的数组开小了!!!

 

posted @ 2022-10-21 11:17  little_sheep_xiaoen  阅读(159)  评论(0编辑  收藏  举报