图论-单源最短路径算法(拓扑,Dijkstra,Floyd,SPFA)

前言

单源最短路径是学习图论算法的入门级台阶,但刚开始看的时候就蒙了,什么有环没环,有负权没负权,下面就来总结一下求单源最短路径的所有算法以及其适用的情况。

单源最短路径

设定图中一个点为源点,求其他所有点到源点的最短路径。

先声明一点:有负环的图中没有最短路径

因为负环绕一圈的权值和是负的,只要过一遍环,路径就减小,可以反复过,无限减小

 

1. 无环 无负权 图求单源最短路径--拓扑排序

求v到其他所有点的最短路径

归纳假设

已知v到前面n-1个点的最短路径

求n

假设每一个点带一个属性sp,表示该点到源点的最短路径长度

设第n个点为z

我们知道所有能到z的点wk,即(wk, z)是图中的边

这样求出来wk.sp + len(wk, z)的最小值

就是z的sp

为什么要拓扑排序?

拓扑排序保证看n的时候,与n连接的前面的所有点都看了

复杂度O(V + E)


伪代码:

const int MAX = MAX_INT
for(all w){

    w.sp = MAX

}

init w.indegree DFS

w with 0 indegree, enqueue


while(!queue.empty()){

    w = dequeue();

    for(all (w, z)){

        if(w.sp + len(w, z) < z.sp){

            z.sp = w.sp + len(w, z);

        }

        z.indegree--;

        if(z.indegree = 0){

            z.enqueue();

        }

    }

}


 

2. 有环 无负权 图求单源最短路径--Dijkstra算法

前面拓扑排序的求所有点的最短路径只能是无环图

有环会导致拓扑排序无法终止

没法用

Dijkstra算法可以解决有环 无负权最短路径的问题

Dijkstra算法是求源点到其他所有点的最短路径

定义两个集合

一个集合是S, 表示已经确定了最短路径的点

另一堆是剩下的所有点M的集合

Init-每个点的路径长度设为无穷

每次从剩下的点中取路径长度最小的加入到S中

开始

只有源点在S中

计算源点所有连接点的路径长度

取出最小x的加入到S中

并对所有连接x的点更新路径长度

再挑出最小的

反复

直到所有点都加到S中

解决了

 

注意:这里与prim算法有点像

Diljstra算法是不断从剩下的点中选取路径最小的加入的S中

prim算法是不断从剩下的点中选取能到S且边的权值最小的加入到S中

伪代码:

const int MAX = MAX_INT;
for(all w except v in G){

    w.sp = MAX;

    w.mark = false;

}

v.sp = 0;

build a heap for all vetex;

while(there exist unmarked vertex){

    w = heap.pop();    //w.sp is minimum

    w.mark = true;

    for(all (w, z) in G and z is unmarked){

        if(w.sp + len(w, z) < z.sp){

            z.sp = w.sp + len(w, z);

        }

    }

    heapsort();

}

 

build the Heap  // O(VlogV)

最多需要E次更新,每一次更新O(logv)

从而总时间

O((E+V)logV)

问:为什么Dijkstra算法不能求解带负权的路径的图的最短路径?

答:采用dijkstra算法处理带有负权边的图时有可能出现这样一种情况:因为dijktra算法每一次将一个节点加入已访问集合之中,之后不能在更新,

如图

图论-单源最短路径算法(拓扑,Dijkstra,Floyd,SPFA)

 

算法刚开始执行时,将A添加到集合中标记已访问,之后选出从A到所有节点中的最短的d,于是把C加入集合中标记已访问,之后C不能在更新了,而显然,A与C之间最短路径权值为0(A-B-C),发生错误。

 

3. 有环 有负权 图的单源最短路径--Floyd算法

可以用于带负权的图

Floyd算法

动态规划

C[i][j] 表示i点到j点的最短距离

小问题可解-所有相邻的边的权值已知

大问题分解为小问题

从i到j

两种途径

1.图中有边连接i与j

2.经过k点到,C[i][j] = C[i][k] + C[k][j]

1 <= k <= N

两种的最小值就是C[i][j]

 

定义矩阵C[N][N]

然后不断dp

 

伪代码:

const int MAX = MAX_INT;

c[N][N]

for(i = 1; i <= N; i++){

    for(j = 0; i <= N; j++){

        if((i, j) is in G ){

            c[i][j] = (i, j).weight;

        }

        else{

            c[i][j] = MAX;

        }

    }

}

 

for(k = 1; k <= N; j++){

    for(i = 1; i <= N; i++){

        for(j = 0; j <= N; j++){

            if(c[i][j] > c[i][k] + c[k][j]){

                c[i][j] = c[i][k] + c[k][j];

            }

        }

    }

}

 复杂度O(n^3)

注意k只能放在最外层的循环

不能放在最里层

若放在最里层

计算第一行eg (1, 10)时

要算(1, 5) (5, 10)

但这时(5, 10)还未知

所以确定的(1, 10)点的最短路径是不准确 的

k放最外面的循环中

保证计算每次看计算C[i][j]时, C[i][k] + C[k][j]都是已经求过的值

 

4. 有环 有负权 图单源最短路径算法--SPFA

带有负权的图

不能使用Dijkstra算法

floyd算法又复杂度太高

然后西安交通大学acm大牛想出了SPFA

中华少年英才多啊

SPFA

设立一个队列

d[i]表示源点到i的最短距离

init所有d[i]都为无穷

刚开始源点入队

x出队

检查x连接的所有点d[i]值,如果变小,则跟新

更新了的点y,说明与y连接的点也有可能更新

y入队

之后不断反复

因为点数有限,所以必然一定步骤后队列会为空

这时就求得了所有点的最短路径

真TMD神奇

检查每个点连接的所有点,总检查就是边数E

每个点可能入队多次,设平均入队次数为k

复杂度O(kE)

可以证明k<=2

 

Spfa 有两种实现方法:bfs,dfs

bfs判断起来较麻烦

得计算所有点入队的次数

如果一个点入队超过N次,一定有负环

bfs伪代码:

const int MAX = MAX_INT

spfa_bfs(){

    suppose root = s;

    d[N];     //The min path length of s to i

    vis[N];      //1 -- i is in the queue

    count[N];     //The count of i to enqueue

    for(i = 1; i <= N; i++){

        d[N] = MAX;

    }

    d[s] = 0;

    vis[s] = 1;

    count[s] = 1;

    enqueue(s);

    while(!queue.empty()){

        x = dequeue();

        vis[s] = 0;

        for((x, y) is in G){

            if(d[y] > d[x] + len(x, y)){

                d[y] = d[x] + len(x, y);

                vis[y] = 1;

                count[y]++;

                if(count[y] >= N){

                    return -1;     // There is a minus circle

                }

            }

        }

    }

}

 

dfs 伪代码:

判断负环较快

 

vis[N];      //1 -- i is checked

int spfa_dfs(point v){

    vis[v] = 1;

    for((v, w) is in G){

        if(d[w] > d[v] + len(v, w)){

            d[w] = d[v] + len(v, w);

            if(vis[w] == 0){

                if(spfa_dfs(w) == -1){

                    return -1;

                }                               //-1   there is a minus circle

            }

            else{

                return -1;

            }

        }

 

    }

    vis[v] = 0;

}

总结

无环 无负权 图单源最短路径--拓扑排序  

有环 无负权 图单源最短路径--Dijkstra算法

有环 有负权 图单源最短路径--Floyd算法

有环 有负权 图单源最短路径--SPFA

这里总结了计算单源最短路径的四种常见算法,理清楚他们分别适用的情况,以及他们之间的关系便会很好记忆。