SPFA蒜法(SLF与LLL优化)——C语言实现

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等函数均为双端队列的基本操作。

双端队列的实现

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值