CSP-SPFA和负环判定
基础知识
在讲述SPFA之前,我们需要了解的是他的根源算法Bellman-Ford算法。
Bellman-Ford算法
在一个正权图中,求解单源最短路可以使用Dijkstra算法解决,并且具有较好的时间复杂度O((m+n)logn)。但是在很多实际问题中,会出现很多负权边的情况,一旦出现负边dijkstra算法无法保证原先的正确性,Bellman和Ford引入了新的算法来计算带负权的单源最短路求解算法。
在保证Bellman-Ford算法的正确性之前,需要理解如下的事实:
1、如果图中有n个点,单源最短路径经过的边数<点数
2、如果在路径上边数等于或者超过了n,说明某个点被访问了两次,出现了环路
3、当有一条边(u,v),如果dis[u]是最小值,且(u,v)在dis[v]的路径上,那么可以将dis[v]更新成dis[u]+(u,v)的权值
由此我们可以使用暴力模拟法得出Bellman-Ford算法的求解过程:
因为最短路的边最长为n-1,所以调整n-1次,每次调整过程中遍历所有边,保证第i次调整结束后,长度为i的边最小值已经被确定。时间复杂度为O(nm)
Bellman-Ford算法具体实现代码:
struct edge
{
int x;
int y;
int value;
};
edge edges[1000];
for(int i=0;i<=n;i++)
{
dis[i]=inf;
pre[i]=0;
}
dis[s]=0;
//更新n-1趟,每次更新全部边,并不够优秀
for(int k=1;k<n;k++)
for(int i=0;i<=m;i++)
{
if(dis[edges[i].y)>dis[edges[i].x]+edges[i].value)
{
dis[edges[i].y]=dis[edges[i].x]+edges[i].value;
pre[edges[i].y]=edges[i].x;
}
}
虽然这种方法是可行的,但由于边数m的大小未知,在求解稠密图问题时算法很有可能退化到O(n^3),因此我们需要一种较好的优化方式来修改这种算法。
SPFA队列优化
如果你多读两遍上面的代码并且画出他的具体执行图,就会发现Bellman-Ford的执行过程与BFS十分相似,都是一种在图上的扩散最终遍历到能够到达的所有点。因此我们可以思考能否引入BFS中的队列思想来对Bellman-Ford做以优化。
对Bellman-Ford进行实例测试我们可以发现这样的特点:松弛操作只发生在最短路径的前导节点中,已经松弛过的点上。
可能你听的有些乱,那我们举个例子:
第一次松弛:松弛与源点相连的边,找到所有边长为1的最短路
第二次松弛:第一轮被松弛过点作为前导节点松弛,找到所有边长为2的最短路
…
可以发现一个特点:只有在前面被松弛过的点后面才会作为先导节点再次进行松弛。
利用这个特点,类比队列,就可以得到SPFA的实现方法:
1、建立一个队列,队列存储要进行松弛的点,初始化将源点放入到队列中
2、每次从队首取点并松弛其邻接点,如果一个邻接点被松弛且队列中无该元素,则将其加入队列
3、在每次松弛结束后进行判断该条路径上的边数,一旦边数>=n,说明重复访问了某个点,出现了负环。对该点打上标记,并且将于该点联通的所有点都打上标记。在进行后续的松弛时,带有负环标记的点不可以被加入队列,否则程序将死循环。
4、重复2-3至队列为空。
SPFA利用了Bellman-Ford算法的松弛性质,在正常数据情况下,大大提高了性能,时间复杂度为 O(km)(k是一个常数)。
SPFA的具体实现:
queue<int> q;
int cnt[N];
int vis[N];
int pre[N];
int dis[N];
void SPFA(int s)
{
while(q.size()) q.pop();
//q置空
for(int i=1;i<=n;i++)
{
cnt[i]=0,dis[i]=inf,pre[i]=0;vis