spfa 算法 (单源最短路)

本文深入探讨了SPFA算法的原理、应用条件、时间复杂度及实现细节,包括如何处理带负权边的最短路径问题,以及通过队列优化的Bellman-Ford算法。文章还提供了SPFA算法的源码实例,帮助读者理解算法流程并实践应用。

求单源最短路的SPFA算法的全称是:Shortest Path Faster Algorithm。

简单的说就是队列优化的bellman-ford

在路径中存在负权边是 dijkstra 就没法使用了 ,这是就可以SPFA 了

但是当有负权的环是 就没有最短路,spfa 可以判断是否有负权环,如果没有就可以求出最短路。。


期望的时间复杂度O(ke), 其中k为所有顶点进队的平均次数,可以证明k一般小于等于2。  

实现方法:建立一个队列,初始时队列里只有起始点,再建立一个表格记录起始点到所有点的最短路径(该表格的初始值要赋为极大值,该点到他本身的路径赋为0)。然后执行松弛操作,用队列里有的点去刷新起始点到所有点的最短路,如果刷新成功且被刷新点不在队列中则把该点加入到队列最后。重复执行直到队列为空


简单说就是把源点放入队列,然后松弛这个点相连的点,如果松弛成功就把这个点放入队列,用一个数组记录每个点是否在队列中,在一次取出队列中的点,在松弛相连的点,如果松弛成功,就判断这个点是不是在队列中,如果不再队列中就把这个点放入队列(点可能多次进入队列

spfa 可以判断有无负权环,只需 记录每个点进队的次数,如果超过了 n-1 就有负权环,因为每个点松弛的次数不可能超多n-1

以下的源码 (引自:http://www.nocow.cn/index.php/SPFA

/*
 * 单源最短路算法SPFA,时间复杂度O(kE),k在一般情况下不大于2,对于每个顶点使用可以在O(VE)的时间内算出每对节点之间的最短路
 * 使用了队列,对于任意在队列中的点连着的点进行松弛,同时将不在队列中的连着的点入队,直到队空则算法结束,最短路求出
 * SPFA是Bellman-Ford的优化版,可以处理有负权边的情况
 * 对于负环,我们可以证明每个点入队次数不会超过V,所以我们可以记录每个点的入队次数,如果超过V则表示其出现负环,算法结束
 * 由于要对点的每一条边进行枚举,故采用邻接表时时间复杂度为O(kE),采用矩阵时时间复杂度为O(kV^2)
 */
#include<cstdio>
#include<vector>
#include<queue>
#define MAXV 10000
#define INF 1000000000 //此处建议不要过大或过小,过大易导致运算时溢出,过小可能会被判定为真正的距离
 
using std::vector;
using std::queue;
 
struct Edge{
	int v; //边权
	int to; //连接的点
};
 
vector<Edge> e[MAXV]; //由于一般情况下E<<V*V,故在此选用了vector动态数组存储,也可以使用链表存储
int dist[MAXV]; //存储到原点0的距离,可以开二维数组存储每对节点之间的距离
int cnt[MAXV]; //记录入队次数,超过V则退出
queue<int> buff; //队列,用于存储在SPFA算法中的需要松弛的节点
bool done[MAXV]; //用于判断该节点是否已经在队列中
int V; //节点数
int E; //边数
 
bool spfa(const int st){ //返回值:TRUE为找到最短路返回,FALSE表示出现负环退出
	for(int i=0;i<V;i++){ //初始化:将除了原点st的距离外的所有点到st的距离均赋上一个极大值
		if(i==st){
			dist[st]=0; //原点距离为0;
			continue;
		}
		dist[i]=INF; //非原点距离无穷大
	}
	buff.push(st); //原点入队
	done[st]=1; //标记原点已经入队
	cnt[st]=1; //修改入队次数为1
	while(!buff.empty()){ //队列非空,需要继续松弛
		int tmp=buff.front(); //取出队首元素
		for(int i=0;i<(int)e[tmp].size();i++){ //枚举该边连接的每一条边
			Edge *t=&e[tmp][i]; //由于vector的寻址速度较慢,故在此进行一次优化
			if(dist[tmp]+(*t).v<dist[(*t).to]){ //更改后距离更短,进行松弛操作
				dist[(*t).to]=dist[tmp]+(*t).v; //更改边权值
				if(!done[(*t).to]){ //没有入队,则将其入队
					buff.push((*t).to); //将节点压入队列
					done[(*t).to]=1; //标记节点已经入队
					cnt[(*t).to]=1; //节点入队次数自增
					if(cnt[(*t).to]>V){ //已经超过V次,出现负环
						while(!buff.empty())buff.pop(); //清空队列,释放内存
						return false; //返回FALSE
					}
				}
			}
		}
		buff.pop();//弹出队首节点
		done[tmp]=0;//将队首节点标记为未入队
	}
	return true; //返回TRUE
} //算法结束
 
int main(){ //主函数
	scanf("%d%d",&V,&E); //读入点数和边数
	for(int i=0,x,y,l;i<E;i++){
		scanf("%d%d%d",&x,&y,&l); //读入x,y,l表示从x->y有一条有向边长度为l
		Edge tmp; //设置一个临时变量,以便存入vector
		tmp.v=l; //设置边权
		tmp.to=y; //设置连接节点
		e[x].push_back(tmp); //将这条边压入x的表中
	}
	if(!spfa(0)){ //出现负环
		printf("出现负环,最短路不存在\n");
	}else{ //存在最短路
		printf("节点0到节点%d的最短距离为%d",V-1,dist[V-1]);
	}
	return 0;
}


SPFA(Shortest Path Faster Algorithm)是一种用于计算单源短路径的算法,特别适用于图中存在负权边的情况。它本质上是Bellman-Ford算法的一种优化版本,通过使用队列来减少不必要的松弛操作。在SPFA中,只有那些在上一轮松弛操作中被更新的点才会参与到下一轮的松弛操作中,从而减少了计算量。 ### SPFA算法的基本思想 SPFA算法的核心在于利用广度优先搜索(BFS)的方式对图进行遍历,并且通过松弛操作来逐步找到从起点到其他所有顶点的短路径。该算法能够处理带有负权边的图,但不能处理存在负权环的情况。如果图中存在负权环,则算法可能会陷入无限循环[^3]。 ### SPFA算法的实现步骤 1. 初始化距离数组`dist[]`,将起点的距离设为0,其余顶点的距离设为无穷大。 2. 创建一个队列,并将起点加入队列。 3. 使用一个布尔数组`visit[]`来记录顶点是否已经在队列中,以避免重复入队。 4. 当队列不为空时,取出队首顶点,对其所有的邻接顶点进行松弛操作。 5. 如果某个邻接顶点的距离可以通过当前顶点得到更小的值,则更新该邻接顶点的距离,并将其加入队列(如果它不在队列中)。 6. 重复上述过程直到队列为空。 ### SPFA算法的C++实现 以下是一个简的C++实现示例,展示了如何使用SPFA算法来求解单源短路径问题: ```cpp #include <iostream> #include <queue> #include <vector> #include <climits> using namespace std; struct Edge { int to, weight; }; void spfa(int start, vector<vector<Edge>>& graph, vector<int>& dist) { int n = graph.size(); vector<bool> inQueue(n, false); queue<int> q; dist[start] = 0; q.push(start); inQueue[start] = true; while (!q.empty()) { int u = q.front(); q.pop(); inQueue[u] = false; for (auto& edge : graph[u]) { int v = edge.to; int w = edge.weight; if (dist[v] > dist[u] + w) { dist[v] = dist[u] + w; if (!inQueue[v]) { q.push(v); inQueue[v] = true; } } } } } int main() { int n, m; cin >> n >> m; vector<vector<Edge>> graph(n); vector<int> dist(n, INT_MAX); for (int i = 0; i < m; ++i) { int u, v, w; cin >> u >> v >> w; graph[u].push_back({v, w}); } int start; cin >> start; spfa(start, graph, dist); for (int i = 0; i < n; ++i) { cout << "Distance from " << start << " to " << i << " is " << dist[i] << endl; } return 0; } ``` ### SPFA算法的时间复杂度分析 SPFA算法的时间复杂度在平均情况下为O(m),其中m是图中的边数。然而,在坏的情况下,时间复杂度可以达到O(n*m),其中n是顶点数。这是因为每个顶点可能被多次加入队列,每次加入队列后都需要对其所有邻接边进行检查[^1]。 ### SPFA算法的应用场景 - **网络路由**:SPFA可以用于计算网络中的短路径,特别是在存在负权边的情况下。 - **交通规划**:在交通网络中,可能存在某些路段因为施工等原因导致通行时间减少,这时可以使用SPFA来计算短路径。 - **社交网络分析**:在分析社交网络中的关系强度时,SPFA可以帮助找到短的关系链。 ### SPFA算法的优缺点 #### 优点 - **处理负权边**:相比Dijkstra算法SPFA能够处理含有负权边的图。 - **效率较高**:相对于Bellman-Ford算法SPFA通过队列优化减少了不必要的松弛操作,提高了效率。 #### 缺点 - **无法处理负权环**:如果图中存在负权环,SPFA算法可能会陷入无限循环。 - **坏情况下的性能较差**:虽然在平均情况下表现良好,但在坏情况下,SPFA的时间复杂度可能不如Dijkstra算法[^4]。 ### SPFA算法Dijkstra算法的对比 - **适用范围**:Dijkstra算法只能处理非负权图,而SPFA可以处理含有负权边的图。 - **实现复杂度**:Dijkstra算法通常使用优先队列实现,而SPFA使用普通队列即可。 - **时间复杂度**:Dijkstra算法的时间复杂度为O((n + m) log n),其中n是顶点数,m是边数;而SPFA的平均时间复杂度为O(m),但在坏情况下为O(n*m)[^3]。 ### SPFA算法的变种 - **DFS版本的SPFA**:在某些特定情况下,如检测负权环时,可以使用深度优先搜索(DFS)版本的SPFA来提高效率[^2]。 通过以上介绍,可以看出SPFA算法在处理单源短路径问题时具有一定的优势,尤其是在存在负权边的情况下。然而,需要注意其局限性,特别是在处理负权环时的表现。 ---
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值