最短路

Floyd算法

BFS求01最短路

题目链接

对于边权只有0和1的图,可以用BFS+deque求最短路

具体做法:前端队列存由0边更新的点,后端存由1更新的点,每次松弛从前端取

比较好想,由于是BFS,可以认为本层转移下,0边转移的点dis都相等,1边转移的点dis都相等(不一定正确),那么从前面取出来的点通过0边松弛了另一个点,dis值完全一样,放进队头也不会影响单调性,而1边松弛的点dis值+1又和后面的值一样,所以大致正确

本质上是dijkstra的特化版本

dijkstra算法

运用贪心策略,在未访问点集中选取距已访问点集最近的点,由此点拓展,堆优化后时间复杂度为\(O((n+m)logm)\)

void dijkstra() {
	for(int i=1;i<=n;i++) dis[i]=INF;
	dis[s]=0;
	q.push(make_pair(0,s));
	while(!q.empty()) {
		int x=q.top().second;
		q.pop();
		if(vis[x]) continue;	//这里注意不要重复访问,不然会TLE
		vis[x]=1;
		bfs(x) {
			int v=e[i].to;
			if(dis[v]>dis[x]+e[i].dis) {
				dis[v]=dis[x]+e[i].dis;
				if(!vis[v]) q.push(make_pair(dis[v],v));
			}
		}
	}
}

最短路DAG

固定源点的所有点最短路的集合是DAG(易证)

利用该性质可以使用一些高效算法(拓扑,DP等)

注意:对于\(v\)点的第一次更新(\(dis[v]:\ INF\ -> dis[x]+e[i].dis\))并不能保证\(dis[v]\)\(v\)的最短路长;优先队列维护的是已完成更新的点集,因此取出的是\(dis[x]\)最小的点,并不能保证\(dis[x]+e[i].dis\)最小

因此应在dijkstra外建DAG

求路径,路径数

由于dijkstra算法是由源点向外拓展的,路径更新有连续性且形象易分析,所以常用于最短路计数

松弛的时候统计就好了

求次短路

Bellman-Ford算法

跑n次松弛操作,每次都把全部m条边松弛,由于最短路不会重复经过点,因此n次松弛后一定能找到最短路

for(int i=0; i<n; i++)
  for(int j=0; j<m; j++)
  {
  	 if(dist[a]+w<dist[b])
  	   dist[b] = dist[a] + w; //w是a->b的权重 
  }

判负环

由上文,我们进行第n+1次操作,如果还能松弛,说明走了重复点,即出现负环

加个flag看是否松弛即可

SPFA算法

基础

队列优化的Bellman-Ford,用队列存储点,找到可以松弛的边再把它连的点加进来。时间复杂度\(O(km)\),遇到网格图会退化成\(O(nm)\)

bool SPFA() {
	for(int i=1;i<=n;i++) vis[i]=0,dis[i]=INF;
	queue<int> q;
	q.push(1)
	while(!q.empty()) {
		int x=q.front();
		q.pop();
		vis[x]=0;
		bfs(x) {
			int v=e[i].to;
			if(dis[v]>dis[x]+e[i].dis) {
				dis[v]=dis[x]+e[i].dis;
				if(!vis[v]) {
					q.push(v);
					vis[v]=1;
				}
			}
		}
	}
	return 0;
}

p.s.此份代码默认无负环,往往使用SPFA都是用于判负环。

注意加判是否入队,否则会重复操作

优化

1、DFS优化

广搜改深搜

2、SLF优化

3、LLL优化

判负环

1、边做最短路边判负环

一个点最多只能被n-1个点松弛入队,若入队次数>=n,则有重复经过,即找到负环。

在做最短路时加个cnt[]存入队次数即可。时间复杂度\(O(nm)\)

bool SPFA() {
	for(int i=1;i<=n;i++) dis[i]=1e9;
	deque<int> q;		//SLF优化
	q.push_front(0);
	vis[0]=1;
	while(!q.empty()) {
		int x=q.front();
		q.pop_front();
		vis[x]=0;
		for(int i=head[x];i;i=e[i].next) {
			if(dis[e[i].to]>dis[x]+e[i].dis) {
				dis[e[i].to]=dis[x]+e[i].dis;
				cnt[e[i].to]++;
				if(cnt[e[i].to]>n) return 1;	//找到负环
				if(!vis[e[i].to]) {
					if(!q.empty()&&dis[e[i].to]<dis[q.front()]) q.push_front(e[i].to);
					else q.push_back(e[i].to);
					vis[e[i].to]=1;
				}
			}
		}
	}
	return 0;
}

2、只判负环

又看不懂了……

【模板】负环 里dfs SPFA被卡了(link

学长说用bfs版就ok(有朝一日我会来弄清楚这个问题的!)

与第一种判负环方法不同的是——BFS改DFS,初始化dis[]为0,再从每个点跑SPFA,每次跑不需要初始化dis[]。

1、初始化dis[]为0时,SPFA会且仅会进入负权子路径并松弛dis[]为负数,而负环也是负权子路径的一种,因此负环肯定能走到

由于是从每个点都跑,不会被某些正权边卡

当然,初始化dis[]为0注定了它不能跑最短路

2、每次跑不需要初始化dis[]是因为如果\(dis[v]>dis[x]+e[i].dis\),那么松弛可能会使后面dis值更小,更有可能找到负环;相反,如果\(dis[v] \le dis[x]+e[i].dis\),比现在搜出的dis[]还小的路径早就搜过了,那就完全没有进去搜的必要了

如果每次跑都清dis[],那就会导致上面说的“没有搜的必要”的情况进去搜了,做无用功

最坏时间复杂度是\(O(nm)\),但是往往达不到

bool SPFA(int x) {
	vis[x]=1;
	bfs(x) {
		int v=e[i].to;
		if(dis[v]>dis[x]+e[i].dis) {
			dis[v]=dis[x]+e[i].dis;
			if(vis[v]||SPFA(v)) return 1; 
		}
	}
	vis[x]=0;
	return 0;
}
bool Check() {
	for(int i=1;i<=n;i++) vis[i]=0,dis[i]=0;
	for(int i=1;i<=n;i++) if(SPFA(i)) return 1;
	return 0;
}

Johnson算法

posted @ 2024-10-31 17:21  Zhone_lb  阅读(14)  评论(0)    收藏  举报