Dijkstra算法可以较快的解决单源最短路径问题,并且SPFA算法时间复杂度更大,那我们为什么还要用SPFA呢,在有些问题中,权值是有负值的情况,但是Dijkstra不能解决负权值,这时候就需要我们用SPFA算法了。
SPFA本质上算是Bellman-Ford的优化,由于Bellman-Ford时间复杂度过高,我们一般更偏爱SPFA,SPFA可以解决负权值问题,但是无法处理负环的情况,我们可以事先通过拓扑排序判断图中是否存在负权回路,所以我们先设定我们解决的是有向加权无负环图G的最短路径问题。
算法详解
我们用一个数组dist记录每个结点的最短路径估计值,用邻接表存储图,用一个双端队列q来保存等待优化的结点(为什么用双端队列,方便后续的SLF优化),每次优化取队首结点u,用当前条件下u的最短路径,对所有与u相连的结点v进行松弛操作,如果v被松弛,并且v不在队列中,则将v入队,直到队列为空。
操作步骤
(1)初始化dist数组为INF(无穷大)
(2)源点入队,遍历队首顶点p可以拓展的边,若队首的边<p,v>可以松弛与队首顶点p相连的顶点v,所谓松弛即当满足dist[v] > dist[u] + <u,v>时,更新dist[v] = dist[u] + <u,v>。更新dist[v]。
(3)若v不在队列中,则将v入队,否则继续遍历与队首相连的下一个顶点。
(4)循环(2),(3)直到队列为空,松弛遍历图中所有的顶点。
优化
SPFA时间复杂度相较于Bellman-Ford已经优化了很多,但是有的时候还是太慢,不能满足我们的需求,这时候,我们还有两种优化SPFA的方式,如下:
一、SLF:Small Label First优化
优化思路:
采用双端队列,对于将要加入队列的顶点p,判断如果dist[p]小于队首顶点u的dist[u],则将其插入到对头,否咋将其插入到队尾。(这样可以保证每一次队列的队首顶点u的dist都是最小的,权值小意味着可以松弛更多的结点,所以能达到优化的目的)
二、LLL:Large Label Last优化
优化思路:
对于每个要出队的队首顶点u,比较dist[u]和队列中点的dist的平均值,当dist[u]大于平均值时,将其弹出放到队尾,循环取队首顶点的操作,直到队首顶点的dist值小于平均值的时候为止。(同SLF一样,都是尽可能的松弛更多的结点,以优化SPFA的速度)
事实上,这两种优化是互不干扰的,我们可以同时采取这两种优化,网上的资料显示,SLF可提升10%—20%,而SLF+LLL可以提升近50%。但是通常SPFA的时间效率不大稳定,如果不是负权值,还是采用Dijkstra更方便一些。
代码实现
数据结构
typedef struct ANode {
int adjvex;
struct ANode *nextarc;
int weight;
}ArcNode; //边结点的类型
typedef struct Vnode {
int info;
ArcNode *firstarc;
}VNode; //头节点类型
typedef struct {
VNode adjlist[MAXV];
int n, e;
}AdjGraph; //邻接表
算法代码
int SPFA(AdjGraph G, int u, int v) { 邻接表G,源点u,终点v
Queue q;
ArcNode *p;
Initdeque(&q);
int dis[Max], vis[Max] = { 0 }; //dis数组记录路径长度, vis数组标记遍历过的点
int pre[Max], sum, num; //pre数组记录前驱结点,用于输出最短路径
int path_SPFA[2][Max]; //path_SPFA[0]存储u -> v倒序最短路径,path_SPFA[1]存正序
memset(pre, -1, sizeof pre);
memset(dis, 0x3f, sizeof dis);
dis[u] = 0, vis[u] = 1;
Push_Rear(&q, u);
num = 1, sum = dis[u];
while (q.size != 0) {
ElemType t = GetFront(&q);
while (num * dis[t] > sum) { //LLL优化
ElemType s = Pop_Front(&q);
Push_Rear(&q, t);
t = GetFront(&q);
}
ElemType s = Pop_Front(&q);
vis[t] = 0;
num--; sum -= dis[t];
p = G.adjlist[t].firstarc;
while (p != NULL) {
ElemType vex = p->adjvex;
int weight = p->weight;
if (dis[vex] > dis[t] + weight) {
int temp = dis[vex];
dis[vex] = dis[t] + weight;
pre[vex] = t;
if (!vis[vex]) {
vis[vex] = 1;
if (q.size != 0) { //SLF优化
ElemType topf = GetFront(&q);
if (dis[vex] < dis[topf])
Push_Front(&q, vex);
else
Push_Rear(&q, vex);
}
else
Push_Rear(&q, vex);
num++; sum += dis[vex];
}
}
p = p->nextarc;
}
}
int top = -1; //将pre中存放的前驱结点倒序存入路径输出path[1];
int parent = v;
while (parent != -1) {
top++;
path_SPFA[0][top] = parent;
parent = pre[parent];
}
cntSPFA = top + 1; //cntSPFA记录数组长度
for (int i = 0; i < cntSPFA; i++)
path_SPFA[1][i] = path_SPFA[0][cntSPFA - i - 1];
return dis[v]; //返回u -> v 的最短路径长度
}
PS:代码中INF表示无穷大,Max表示数组长度,读者可自行设置,笔者的SPFA只返回了最短路径长度,若想得到最短路径,可返回path_SPFA[1]的首地址。代码中使用的Push_Rear,Push_Rear,Pop_Front,Get_Front等函数均为双端队列的基本操作。