Bellman-Ford算法和SPFA算法

今天也是为了cc,努力奋斗的一天ヾ(≧▽≦*)o

Bellman-Ford算法

引言

前面在学习Dijkstra算法时提到,Dijkstra算法不适用于边的权值为负数的情况。为了解决这个负边权这个问题,就需要使用Bellman-Ford算法(简称BF算法)。和Dijkstra算法一样,Bellman-Ford算法可解决单源最短路径问题,而且在此基础上也能处理负边权的情况。

基本思想

与Dijkstra算法相同,Bellman-Ford算法设置一个数组d,用来存放从源点到达各个顶点的最短距离。同时Bellman-Ford算法返回一个bool值:如果其中存在从源点可达的负环,那么函数将返回false;否则,函数将返回true,此时数组d中存放的值就是从源点到达各顶点的最短距离。

伪代码

需要对图中的边进行V-1轮操作,每轮都遍历图中的所有边:对每条边u -> v,如果以u为中介点可以使d[v]最小,即d[u]+length[u -> v] < d[v]成立时,就用d[u] + length[u -> v]更新d[v]。同时也可以看出,Bellman-Ford算法的时间复杂度是O(VE),其中V是顶点的个数,E是边数。

for(i = 0;i < n - 1;i++){	//执行n-1轮操作,其中n为顶点数 
	for(each edge u->v){	//每轮操作都遍历所有边 
		if(d[u] + length[u -> v] < d[v]){	//以u为中介点可以使d[v]更小 
			d[v] = d[u] + length[u -> v];	//松弛操作 
		}
	} 
} 

此时,如果图中没有从源点可达的负环,那么数组d中的所有值都应当已经达到最优。因此,如下面的伪代码所示,只需再对所有边进行一轮操作,判断是否有某条边u -> v仍然满足d[u] + length[u->v] < d[v],如果有,则说明图中有从源点可达的负环,返回false;否则,说明数组d中的所有值都已经达到最优,返回true。

for(each edge u -> v){	//对每条边进行判断 
	if(d[u] + length[u -> v] < d[v]){	//如果仍可以被松弛 
		return false;	//说明图中有从源点可达的负环 
	}
	return true;
} 

邻接表实现

由于Bellman-Ford算法需要遍历所有的边,显然使用邻接表会比较方便;如果使用邻接矩阵,则时间复杂度会上升到 O ( V 3 ) O(V^3) 。因此,下面的代码将使用邻接表作为举例:

struct Node{
	int v,dis;	//v为邻接边的目标顶点,dis为邻接边的边权 
};

vector<Node> Adj[MAXV];	//图G的邻接表
int n;	//n为顶点数,MAXV为最大顶点数
int d[MAXV];	//起点到达各点的最短路径长度

bool Bellman(int s){	//s为源点 
	fill(d,d+MAXV,INF);	//fill函数将整个d数组赋值为INF(慎用memset)
	d[s] = 0;	//起点s到达自身的距离为0
	
	//以下是求解数组d的部分
	for(int i=0;i<n-1;i++){	//执行n-1轮操作,n为顶点数 
		for(int u=0;u<n;u++){	//每轮操作都遍历所有边 
			for(int j = 0;j < Adj[u].size();j++){
				int v = Adj[u][j].v;	//邻接边的顶点
				int dis = Adj[u][j].dis;	//邻接边的边权
				if(d[u] + dis < d[v]){	//以u为中介点可以使d[v]更小 
					d[v] = d[u] + dis;	//松弛操作 
				} 
			}
		} 
	}
	
	//以下为判断负环的代码
	for(int u=0;u < n;u++){	//对每条边进行判断 
		for(int j=0;j<Adj[u].size();j++){
			int v = Adj[u][j].v;	//邻接边的顶点
			int dis = Adj[u][j].dis;	//邻接边的边权
			if(d[u] + dis < d[v]){	//如果仍可以被松弛 
				return false;	//说明图中有从源点可达的负环 
			} 
		}
	}
	
	return true;	//数组d的所有值都可以达到最优 
} 

非常重要的一点

统计最短路径条数的做法:由于Bellman-Ford算法期间会多次访问曾经访问过的顶点,如果单纯按照Dijkstra算法中的num数组的写法,会反复累计已经计算过的顶点。为了解决这个问题,需要设置记录前驱的数组set<int> pre[MAXV],当遇到一条和已有最短路径长度相同的路径时,必须重新计算最短路径条数。

SPFA

引言

虽然Bellman-Ford算法的思路很简洁,但是O(VE)的时间复杂度确实很高,在很多情况下并不尽人意。仔细思考后发现,Bellman-Ford算法的每轮操作都需要操作所有的边,显然这其中有大量无意义的操作,严重影响了算法的性能。

基本思想

于是,注意到,只有当某个顶点u的d[u]值改变时,从它出发的边的邻接点v的d[v]值才有可能被改变。由此可以进行一个优化:建立一个队列,每次将队首顶点u取出,然后对从u出发的所有边u -> v进行松弛操作,也就是判断d[u] + length[u -> v]< d[v]是否成立,如果成立,则用d[u] + length[u -> v]覆盖d[v],于是d[v]获得了更优的值,此时如果v不在队列中,就把v加入到队列中。这样操作直到队列为空(说明图中没有源点可达的负环),或是某个顶点的入队次数超过V-1(说明图中存在从源点可达的负环)。

伪代码

下面是伪代码:

queue<int> Q;
源点s入队;
while(队列非空){
	取出队首元素u;
	for(u的所有邻接边 u-> v){
		if(d[u] + dis < d[v]){
			d[v] = d[u] + dis;
			if(v当前不在队列){
				v入队;
				if(v入队次数大于n-1){
					说明有可达负环,return; 
				} 
			} 
		}
	} 
} 

分析

  • 这种优化后的算法被称为SPFA(Shortest Path Faster Algorithm)

  • 它的期望时间复杂度是O(KE),其中E是图的边数,k是一个常数,在很多情况下k不超过2,可见这个算法在大部分数据时异常高效,并且经常性地优于堆优化的Dijkstra算法。但是如果图中有从源点可达的负环时,传统SPFA的时间复杂度会退化成O(VE)

  • 理解SPFA的关键是理解它是如何从Bellman-Ford算法中优化得到的

邻接表实现

vector<Node> Adj[MAXV];		//图G的邻接表
int n,d[MAXV],num[MAXV];	//num数组记录顶点的入队次数
bool inq[MAXV];				//顶点是否在队列中

bool SPFA(int s){
	//初始化部分
	memset(inq,false,sizeof(inq));
	memset(num,0,sizeof(num));
	
	//源点入队部分
	queue<int> Q;
	Q.push(s);				//源点入队
	inq[s] = true;			//源点已入队
	num[s]++;				//源点入队次数加1
	d[s] = 0;				//源点的d值为0
	
	//主体部分
	while(!Q.empty()){
		int u = Q.front();		//队首顶点编号为u
		Q.pop();				//出队
		inq[u] = false;			//设置u为不在队列中
		
		//遍历u的所有邻接边v
		for(int j=0;j<Adj[u].size();j++){
			int v = Adj[u][j].v;
			int dis = Adj[u][j].dis;
			
			//松弛操作
			if(d[u] + dis < d[v]){
				d[v] = d[u] + dis;
				if(!inq[v]){	//如果v不在队列中 
					Q.push(v);	//v入队
					inq[v] = true;	//设置v为在队列中
					num[v++];		//v的入队次数加一
					if(num[v] >= n){
						return false;	//有可达负环 
					} 
				}
			} 
		}	 
	} 
} 

关于优化SPFA

一些描述

发布了15 篇原创文章 · 获赞 0 · 访问量 213

猜你喜欢

转载自blog.csdn.net/yc_cy1999/article/details/103950243
今日推荐