为避免各种求最短路的方法混淆,开始之前先做个归纳。
① BFS - 无权图 (有向或无向,有环或无环)- 对于树的bfs,无需判重,因为根本不会重复。对于图的bfs,要有vis[]进行判重,不然一个点可能被多次拓展,极大地浪费时间空间。
② dp - 有向无环图(DAG,带权或不带权)- 很多问题可以转换成DAG上的最短路(当然还有最长路)问题,这个时候,一个点就是一个状态,根据状态转移方程计算,属于最优化问题。对于这里非隐式的图(存在显式结点的),是可以用下面其他最短路算法的。
③ Dijkstra - 正权图(有向或无向,有环或无环,带权或不带权)- 每次取最小dist值的点进集合S(这个点的dist已经确定),对这个点连接的边进行松弛操作。当S包含所有点时所有dist求出(对于无法到达的点视情况而定)。
④ Bellman-Ford - 无负环的图(其余随便)- 所谓无负环,即图中若成环,则不能包含有负权的边(否则最短路显然不存在)。进行n-1轮
松弛操作,每轮对所有边进行松弛, 即dist[v] = min{dist[v], dist[u]+w[u][v]}.
⑤SPFA - 同Bellman - SPFA是Bellman的队列优化,减少了不必要的冗余计算。详见下面介绍。
⑥Floyd - 任何图- 求每两点间的最短路。Floyd-Warshall 算法用来找出每对点之间的最短距离。它需要用邻接矩阵来储存边,这个算法通过考虑最佳子路径来得到最佳路径。
一、Bellman-Ford
显然最短路是不含环的(可以用反证法思考),所以最短路除顶点外一定经过n-1条边,对每条边进行n-1次松弛操作。
显然时间复杂度O(VE)。
因为算法简单,适用范围又广,虽然复杂度稍高,仍不失为一个很实用的算法。
NOCOW
简略代码:
for(int i=0; i<n; i++) dist[i] = INF;
dist[0] = 0;
for(int k=0; k<n-1; k++) //迭代n-1次
for(int i=0; i<m; i++) //检查每条边
{
int x = u[i], y = v[i]; //边集数组实现邻接表
if(d[x] < INF) d[y] = d[y]<d[x]+w[i] ? d[y : d[x]+w[i];
}
二、SPFA
对于Bellman的n-1轮relax(松弛操作),显然对于一轮中有很多时候某些边是无法成功relax的。那怎么样使得只对当前可能能够relax的边进行relax呢?注意到,当一条边relax成功后,dist[u]变小了,这时,才有可能使u指向的点的dist值得到更新(成功relax)。
于是引入FIFO队列,把每次成功relax的点入队。若此点已经在队中就不用再加了,因为反正之后这个点会被用来relax邻点,
若再加就相当于会有冗余计算。这样,每次取出队首元素,当队列为空时,结束。同时除了通过判断队列是否为空来结束循环,
还可以通过 判断有无负环:如果某个点进入队列的次数超过V次则存在负环(SPFA无法处理带负环的图)。
期望的时间复杂度O(kE), 其中k为所有顶点进队的平均次数,可以证明k一般小于等于2。
现在再看刚开始提到的bfs,事实上bfs与SPFA十分相像。在无权图中,bfs首先到达的顶点所经历的路径一定是最短路(也就是
经过的最少顶点数)。所以此时利用vis[],可以使每个顶点只进队一次。考虑若用bfs来做带权图,最先到达的顶点所计算出来的路径
不一定是最短路。一个解决方法是放弃vis数组,此时所需时间自然就是指数级的。所以我们不能放弃vis数组,
而是在处理一个已经在队列中且当前所得的路径比原来更好的顶点时,直接更新最优解。
再看NOCOW中关于二者的说法。
SPFA 在形式上和宽度优先搜索(bfs)非常类似,不同的是宽度优先搜索中一个点出了队列就不可能重新进入队列,但是SPFA中一个点可能在出队列之后再次被放入队列,也就是一个点改进过其它的点之后,过了一段时间可能本身被改进,于是再次用来改进其它的点,这样反复迭代下去。
这样对SPFA的理解就更透彻了。
详细步骤:
1.初始化dist[st] = 0; 其余dist = INF; st入队;
2.u = 出队; 对(u, v) relax; relax成功的v,若此时不在队中则入队;
3.重复第2步,直到队空。
代码:
①by Rujia Liu
#include<cstdio>
#include<cstring>
#include<queue>
using namespace std;
const int INF = 1000000000;
const int MAXN = 1000;
const int MAXM = 100000;
int n, m;
int first[MAXN], d[MAXN];
int u[MAXM], v[MAXM], w[MAXM], next[MAXM];
int main() {
scanf("%d%d", &n, &m);
for(int i = 0; i < n; i++) first[i] = -1;
for(int e = 0; e < m; e++) {
scanf("%d%d%d", &u[e], &v[e], &w[e]);
next[e] = first[u[e]];
first[u[e]] = e;
}
queue<int> q;
bool inq[MAXN];
for(int i = 0; i < n; i++) d[i] = (i==0 ? 0 : INF);
memset(inq, 0, sizeof(inq));
q.push(0);
while(!q.empty()) {
int x = q.front(); q.pop();
inq[x] = false;
for(int e = first[x]; e != -1; e = next[e]) if(d[v[e]] > d[x]+w[e]) {
d[v[e]] = d[x] + w[e];
if(!inq[v[e]]) {
inq[v[e]] = true;
q.push(v[e]);
}
}
}
for(int i = 0; i < n; i++)
printf("%d/n", d[i]);
return 0;
}
②
关于SPFA的优化:
SPFA算法有两个优化算法 SLF 和 LLL。
SLF:Small Label First 策略,设要加入的节点是j,队首元素为i,若dist(j)<dist(i),则将j插入队首,否则插入队尾。
LLL:Large Label Last 策略,设队首元素为i,队列中所有dist值的平均值为x,若dist(i)>x则将i插入到队尾,查找下一元素,直到找到某一i使得dist(i)<=x,则将i出对进行松弛操作。
引用网上资料,SLF 可使速度提高 15 ~ 20%;SLF + LLL 可提高约 50%。 在实际的应用中SPFA的算法时间效率不是很稳定,为了避免最坏情况的出现,通常使用效率更加稳定的Dijkstra算法。
①SLF:
②LLL: