最短路径问题:SPFA(Shortest Path Faster Algorithm)算法

1、理论准备

      为了学习网络流,先水一道spfa

      SPFA算法是1994年西南交通大学段凡丁提出,只要最短路径存在,SPFA算法必定能求出最小值,SPFABellman-Ford算法优化的关键之处在于意识到:只有那些在前一遍松弛中改变了距离估计值的点,才可能引起他们的邻接点的距离估计值的改变。为什么队列为空就不改变了呢?就是因为要到下一点必须经过它的前一个邻接点。。SPFA可以处理负权边。很多时候,给定的图存在负权边,这时类似Dijkstra等算法便没有了用武之地,而Bellman-Ford算法的复杂度又过高,SPFA算法便派上用场了。简洁起见,我们约定有向加权图G不存在负权回路,即最短路径一定存在。当然,我们可以在执行该算法前做一次拓扑排序,以判断是否存在负权回路。

      初始化: dis数组全部赋值为Inf(无穷大,不能是map[s][i]),path数组全部赋值为s(即源点),或者赋值为-1,表示还没有知道前驱,然后dis[s]=0;  表示源点不用求最短路径,或者说最短路就是0。将源点入队;另外记住在整个算法中有顶点入队了要记得标记vis数组,有顶点出队了记得消除那个标记(可能多次入队)

      核心:读取队头顶点u,并将队头顶点u出队(记得消除标记);将与点u相连的所有点v进行松弛操作,如果能更新估计值(即令d[v]变小),那么就更新,另外,如果点v没有在队列中,那么要将点v入队(记得标记),如果已经在队列中了,那么就不用入队以此循环,直到队空为止就完成了单源最短路的求解。

      判断有无负环:如果某个点进入队列的次数超过N次则存在负环(SPFA无法处理带负环的图),假设这个节点的入度是k(无向权则就是这个节点的连接的边)如果进入这个队列超过k,说明必然有某个边重复了,即成环;换一种思路:用DFS,假设存在负环a1->a2->->an->a1。那么当从a1深搜下去时又遇到了a1,那么直接可以判断负环了所有用。当某个节点n次进入队列,则存在负环,此时时间复杂度为O(n*m),n为节点,m为边。

      SPFA算法有两个优化算法 SLF 和 LLL: SLFSmall Label First 策略,设要加入的节点是j,队首元素为i,若dist(j)<dist(i),则将j插入队首,否则插入队尾。 LLLLarge Label Last 策略,设队首元素为i,队列中所有dist值的平均值为x,若dist(i)>x则将i插入到队尾,查找下一元素,直到找到某一i使得dist(i)<=x,则将i出对进行松弛操作。 SLF 可使速度提高 15 ~ 20%SLF + LLL 可提高约 50%。 在实际的应用中SPFA的算法时间效率不是很稳定,为了避免最坏情况的出现,通常使用效率更加稳定的Dijkstra算法。个人觉得LLL优化每次要求平均值,不太好,为了简单,我们可以之间用c++STL里面的优先队列来进行SLF优化。


2、把HDU 1874 AC吧

Java代码如下:

import java.util.Comparator;
import java.util.PriorityQueue;
import java.util.Queue;
import java.util.Scanner;

/*
 * HDU 1874
 * 原来一直wa,重写了一遍,AC了
 * SLF优化
 */

public class Main {
	static int n, m;
	static int[][]   map = new int[205][205];
	static int[]     dis = new int[205];
	static boolean[] vis = new boolean[205];
	/*
	 * 路径最大值是10000,不能设置成10005就行,还要考虑和
	 * 也不能是整形最大值,否则一加就溢出了
	 */
	static final int Inf = 0x3f3f3f3f;
	public static void main(String[] args) {
		Scanner sc = new Scanner(System.in);
		// 记录前驱点 。若path[i]=j,表示从s到i的最短路径中i的前一个点是j
		// int[] path;
		int u, v, w;
		while (sc.hasNext()) {
			n = sc.nextInt();
			m = sc.nextInt();
			for (int i = 0; i < 205; i++) {
				for (int j = i; j < 205; j++) {
					map[i][j] = Inf;
					map[j][i] = Inf;
				}
			}
			for (int i = 0; i < m; i++) {
				u = sc.nextInt();//城市1
				v = sc.nextInt();//城市2
				w = sc.nextInt();//路长
				// 多重边
				if (map[u][v] > w) {
					map[v][u] = w;
					map[u][v] = w;
				}
			}
			int s = sc.nextInt();
			int t = sc.nextInt();
			SPFA(s);
			// 题目上有s t<n,所以不必判断dis[t]是否越界
			// 起点终点相同的话答案是0
			if ( dis[t] == Inf) {
				System.out.println(-1);
			} 
			else {
				System.out.println(dis[t]);
			}
		}
	}
	private static void SPFA(int s) {
		for (int i = 0; i < 205; i++) {
			vis[i] = false;
			// 初始化为map[s][i]第一组数据就错了
			dis[i] = Inf;
		}
		dis[s] = 0;
		vis[s] = true;
		Comparator<Integer> cmp = new Comparator<Integer>() {
			public int compare(Integer o1, Integer o2) {
				int i = (int) o1;
				int j = (int) 02;
				if (dis[i] > dis[j]) {
					return 1;
				} else if (dis[i] == dis[j]) {
					return 0;
				} else {
					return -1;
				}
			}
		};
		Queue<Integer> q = new PriorityQueue<Integer>(205, cmp);
		q.clear();
		q.add(s);
		while (!q.isEmpty()) {
			int head = q.poll();
			// 该注意的是有些点可能重复入队,所以出队的点也要重新置未标记
			vis[head] = false;
				for (int i = 0; i < n; i++) {
						// dis[head]不可能是INF,map[head][i]可能是INF
						if (dis[head] + map[head][i] < dis[i]) {
							// path[i] = head
							dis[i] = dis[head] + map[head][i];
							if (!vis[i]) {
								// 用一个数组在此记录入队次数,大于n就存在负环;如何事先判断
								q.add(i);
								vis[i] = true;
							}
						}
				}
		}
	}
}

转载博客链接 :

http://www.cnblogs.com/hxsyl/p/3248391.html


==============================================================================================================================

C++版本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;
}

转载博客链接

http://www.nocow.cn/index.php/SPFA算法

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值