Bellman-Ford算法思路详解及SPFA算法简介

一、Bellman-Ford算法:
 
首先科普一下,Bellman-Ford算法是由发明者Richard Bellman(理查德.贝尔曼, 动态规划的提出者)和Lester Ford命名的,可以处理路径权值为负数时的单源最短路径问题。【Dijkstra算法的贪心思路无法处理负权边】
 
算法核心:Bellman-Ford算法基于动态规划,反复利用已有的边来更新最短距离,Bellman-Ford算法的核心思想时松弛。
如果dist[u]和dist[v]满足dist[v]<=dist[u]+map[u][v],dist[v]就应该被更新为dist[u]+map[u][v]。
反复地利用上式对dist数组进行松弛,如果没有负权回路的话,应当会在n-1次松弛之后结束。
n个顶点图的最短路,最短路中最长的路径上包含所有的n个顶点,即n-1条边。所以对所有边松弛操作,进行n-1轮,就可以求得最短路了。
原因在于考虑对每条边进行松弛1次的时候,得到的实际上是最多经过1条边的最短路径,
对每条边进行两次松弛的时候得到的是至多经过2条边的最短路径……依次类推
如果没有负权回路,那么任意两点间的最短路径至多经过n-1条边,因此经过n-1次操作后应当可以得到最短路径。
如果由负权回路,那么第n次松弛操作仍然会成功,Bellman-Ford算法就是利用这个性质判定负环
#include<bits/stdc++.h>
using namespace std;

const int MN=1005,INF=0x3f3f3f3f;
int d[MN],ans,n,m,en,s;
struct Edge{
	int u,v,w; //u,v为顶点,w为边权
}ed[MN]; //边集数组,不关心边之间的对应关系,没有定义nxt模拟指针字段
/*该图有n个结点,m条边*/
bool Bellman(int s){
	memset(d,0x3f,sizeof(d)); //单源最短路记录数组初始化为INF
	d[s]=0; //源点s,最短路为0
	for(int i=1;i<n;++i) //n-1 次松弛即可
		for(int j=1;j<=m;++j) //每次松弛所有的边
		{
			int tu=ed[j].u,tv=ed[j].v,tw=ed[j].w; 
			if(d[tv]>d[tu]+tw) //松弛检测
				d[tv]=d[tu]+tw; 
		}
	for(int j=1;j<=m;++j)
	{//验证是否有环,如果n次松弛之后,还可以松弛,就是有环
		int tu=ed[j].u,tv=ed[j].v,tw=ed[j].w;
		if(d[tv]>d[tu]+tw)
			return 0; //有环返回0
	}
	return 1;//无环返回1
}

 

 Bellman-Ford 算法优化优化:

循环的提前退出:

  在实际操作中,贝尔曼-福特算法经常会在未达到 |V| - 1 次前就出解,|V| -1 其实是最大值。于是可以在循环中设置判定,在某次循环不再进行松弛时,直接退出循环,进行负权环判定。【类似于冒泡排序的优化】

      由读者自行完成实现,可以用bool变量每轮是否有松弛操作。

 

 

二、SPFA:Bellman-Ford 算法的队列优化实现

是一个用于求解有向带权图单源最短路径的改良的贝尔曼-福特算法(当然也可以通过将每条边换为两条逆向的边来用于无向图)。这一算法被认为在随机的稀疏图上表现出色,并且极其适合带有负边权的图。然而SPFA在最坏情况的时间复杂度与Bellman-Ford算法相同,因此在非负边权的图中仍然最好使用Dijkstra。

原理:

  基于Bellman-Ford之外,再可以确定,松弛操作必定只会发生在最短路径前导节点松弛成功过的节点上,用一个队列记录松弛过的节点,可以避免了冗余计算

优化:

  SPFA算法的性能很大程度上取决于用于松弛其他节点的备选节点的顺序。

       我们注意到其与Dijkstra很像,

       一方面,优先队列替换成普通的FIFO队列,

       而另一方面,一个节可以多次进入队列点

SPFA 代码模板:习题:香甜的黄油 【点击链接打开】

//【例4-6】香甜的黄油——一本通入门篇练习题:
#include<bits/stdc++.h>
using namespace std;
const int INF=0x3f3f3f3f;
int n,p,c,a[501],hd[801],en,ds[801],tot,ans=INF;
bool ext[801];
struct Edge{
	int to,nxt,dis;
}ed[2902]; 

void addEdge(int from,int to,int dis){
	en++;
	ed[en].dis=dis;
	ed[en].to=to;
	ed[en].nxt=hd[from];
	hd[from]=en;
}

void spfa(int s){	
	queue<int> qe;
	qe.push(s);
	ext[s]=1; //起点标记在队列中
	ds[s]=0;
	while(!qe.empty()){
		int qf=qe.front();
		qe.pop();
		ext[qf]=0; //对头结点出队后,标记为不在队列中
		for(int j=hd[qf];j;j=ed[j].nxt){
			int qt=ed[j].to,qw=ed[j].dis;
			
			if(ds[qt]>ds[qf]+qw){
				ds[qt]=ds[qf]+qw;
				
				if(!ext[qt]){ //不在队列中的结点才需要再次入队
					ext[qt]=1;
					qe.push(qt);
				}
			}
		}
	}
}

int main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);
	cout.tie(0);
	cin>>n>>p>>c;
	for(int i=1;i<=n;++i)
		cin>>a[i];		
	
	for(int i=0;i<c;++i){
		int u,v,d;
		cin>>u>>v>>d;
		addEdge(u,v,d);
		addEdge(v,u,d);
	}
	for(int i=1;i<=p;++i){
		memset(ds,0x3f,sizeof(ds));
		memset(ext,0,sizeof(ext));
		spfa(i);  //调用p次spfa()
		tot=0;
		for(int i=1;i<=n;++i)
			tot+=ds[a[i]];
		ans=min(ans,tot);
	}
	cout<<ans<<endl;
	return 0;
}

  事实上,如果 q 是一个优先队列,则这个算法将极其类似于Dijkstra算法。然而尽管这一算法中并没有用到优先队列,仍有两种可用的技巧可以用来提升队列的质量,并且借此能够提高平均性能(但仍无法提高最坏情况下的性能)。两种技巧通过重新调整 q 中元素的顺序从而使得更靠近源点的节点能够被更早地处理。

距离小者优先(Small Lable First(SLF)):【用双端队列】

  将总是把v压入队列尾端改为比较dis[v]与dis[q.front()]的大小(为了避免出现队列为空的操作,先将v压入队尾),并且在v较小时将v压入队列的头端。

距离大者优先(Large Lable Last(LLL)):【用优先队列】

  我们更新队列以确保队列头端的节点的距离总小于平均,并且任何距离大于平均的节点都将被移到队列尾端。

  

 改为DFS版:

  dfs版spfa判环根据:若一个节点出现2次及以上,则存在负环。具有天然的优势。由于是负环,所以无需像一般的spfa一样初始化为极大的数,只需要初始化为0就够了(可以减少大量的搜索,但要注意最开始时for一遍)。

 

 

posted on 2022-04-18 16:10  幽遇  阅读(882)  评论(0编辑  收藏  举报