图的最短路专题

带权图的最短路:

Dijkstra:

无优化\(O(n^2)\)

注意只能用于非负边权!!
当为负边权时在第一步时就会出错
可能dis v为最小且v与源点不直接连通,经过了中间节点,而源点到中间节点的距离大于源点到v的距离,所以源点到中间
节点没有被确定标记,所以顺序乱了,出错了

单源最短路
即从指定点到源点的最短路
设g[u][v]为原图邻接矩阵
pre[v]为v的前驱节点,用来输出路径
dis[v]为源点到节点v的当前最短距离
vis【v】为标记数组,1为已标记
0为未标记

思路如下:

已标记的节点为已确定最短路的节点
未标记的节点为暂时未确定最短路的节点
初始化先将dis【源点】=0
将源点直接相连的所有点的dis改为g【源点】【v】
其它dis赋值正无穷
pre【源点】=0
因为源点无前驱节点
正式开始算法
第一步找出所有未标记节点中dis【v】最小的
记录下这个节点,并将这个节点到源点的真正最短路径定为dis【v】
即dis【v】一定不超过源点到v的其它路径
为什么?
因为如果源点与v之间存在多条路径的话,若存在一条路径能直接从源点到v且比dis【v】短,则在之前的第三步时dis【v】就会被更新为更短路径
若存在一条路径不能直接从源点到v且比dis【v】短,这样的话那条路径就一定会经过一个中间节点k
且dis【k】<dis[v],而这样的话dis【v】就不是最小了
所以dis【v】一定是源点到v的真正最短路径

第二步把v节点做上标记

第三步:
遍历所有与v节点直接相连的未标记节点x
如果dis[v]+g[v][x]<dis[x]
则更新dis【x】=dis[v]+g[v][x]
由此可见这个过程是贪心的
即dis【x】是当前最短的,但不是它真正的最短路
记录点x的前驱节点pre[x]=v
这也是不断变化的
因为此时通过中间节点v到s要比之前的路径短
所以此时x的前驱节点暂时为v
但是当有更短源点到x的路径时x的前驱节点会变成更好的节点v
但是等所有操作完成后dis【x】一定为真实的最短路径,pre【x】也一定为真正的x的前驱节点

不断执行以上三步操作,直到所有节点都已标记

输出路径:

找到要求的终点k
存储k
k=pre【k】
重复以上操作直到k为0停止

然后逆序输出存储k的数组(因为要从源点到k)

程序实现:

由于每次会标记一个点
总共n个点
要标记n次
所以我们假定源点未标记

	#include<iostream>
	#include<cstdio>
	#include<cstring>
	#include<climits>
	using namespace std;
	int g[2001][2001],vis[10001],dis[10001],pre[10001];
	int main(){
		memset(vis,0,sizeof vis);
		memset(g,0x7f,sizeof g);
		memset(dis,0x7f,sizeof dis);
		
		int n,m,s;
		cin>>n>>m>>s;
		for(int i=0;i<m;++i){
			int u,v,l;
			cin>>u>>v>>l;
			if(l<g[u][v])//处理重边,使得u到v的有向边为重边中的最短的 
			g[u][v]=l;//有向图 
			if(u==v)g[u][v]=0;//应忽略自环,一个点到自身最短距离应为0 
			//cout<<g[u][v]<<endl;
		}
		
		//vis[s]=0;
		for(int i=1;i<=n;++i){
			
			dis[i]=g[s][i];//不能颠倒g【i】【s】因为是有向图,从源点到目标点 
		}
		dis[s]=0;//必须在对其它点赋值后再对s赋值0,否则会被正无穷覆盖 
		pre[s]=0;
		for(int i=1;i<=n;++i){
			int dian=0,mindis=INT_MAX;	//dian一定初始化为0!!因为一旦dian没有赋初值就会re 
			for(int j=1;j<=n;++j){
				
				if(!vis[j]&&dis[j]<mindis){
					mindis=dis[j];
					dian=j;
				}
			}
			if(dian==0)break;//因为如果dian没有更新就说明dis[j]都相等, 此时要么这些点的最短路都是相等的,所以都是真正最短路
			//也有可能dis[j]为inf,此时说明这些点到源点不连通,这两种情况都可以提前结束循环提高效率 
			vis[dian]=1;
			for(int j=1;j<=n;++j){
				if(!vis[j]&&g[dian][j]<1e8&&dis[dian]+g[dian][j]<dis[j]){
					dis[j]=dis[dian]+g[dian][j];//g数组dian和j不可颠倒因为是有向图 
					pre[j]=dian;
				}
			}
		}
		
		for(int i=1;i<=n;++i){
			if(dis[n]>1e9){//如果点的距离大于题目给出最大最短路值就说明这个最短路是inf,即不存在,也就是说该点与源点不连通 
			cout<<INT_MAX<<" ";
			
			continue;
		}
			cout<<dis[i]<<" ";
		}
	//	cout<<endl;
//		cout<<dis[n]<<endl;	
		return 0;
	}[n]<<endl;	
		return 0;
	}

其实,优先队列中是要进行排序的,如果a小于b,那就是正确的,不应该将a b交换位置,而如果a大于b, 那说明需要交换位置,所以需要返回一个真值,来告诉优先队列,让小的在前。
所以优先队列重载小于号时若要从小到大排序就return a>b
若要从大到小排序就return a<b;

但是,在sort自定义排序规则时又遵守了,用户习惯,
即从小到大排序return a<b,从大到小排序return a>b;

priority_queue的自动排序前提是数据在队列内修改,即结构体
如果队列外修改不会自动排序
所以重载小于号时不能直接dis【】比较

dijkstra堆优化

#include<iostream> 
#include<cstdio> 
#include<cstring> 
#include<climits> 
#include<vector> 
#include<queue> 
using namespace std;
struct edge{
	int to,dis;
	edge(int to=0,int dis=0):to(to),dis(dis){
	}
};
struct node{
	int point,minpath;
	node(int point,int path):point(point),minpath(path){
	}
	bool operator <(const node &v)const{
		return minpath>v.minpath ;
	}
};
vector<edge>g[100009];
priority_queue<node>edges;
int dis[100009],vis[100009];
int main(){
	memset(dis,0x7f,sizeof dis);
	memset(vis,0,sizeof vis);
	int n,m,s;
	cin>>n>>m>>s;
	for(int i=0;i<m;++i){
		int a,b,l;
		cin>>a>>b>>l;
		if(a==b)l=0;
		g[a].push_back(edge(b,l)); 
	}
	for(int i=0;i<g[s].size() ;++i){
		dis[g[s][i].to]=g[s][i].dis;
	}
	dis[s]=0;
	edges.push(node(s,0)); //别忘了源点入队! 
	while     (!edges.empty() ){
		node k=edges.top() ;
		edges.pop();
		if(vis[k.point ])continue;
		vis[k.point]=1;
		dis[k.point]=k.minpath ;
		for(int i=0;i<g[k.point ].size();++i){
			int ldis=g[k.point ][i].dis;
			int lto=g[k.point][i].to;
			if(!vis[lto]){//如果没有确定该最短路才执行松弛 
				edges.push(node(lto,k.minpath +ldis)); //不用比较了,直接将松弛后长度入队,无论长度变长还是变短队列都会把短的那个排在前面,长的那个自然被访问过,所以跳过 
			}
		}
		
	}
	for(int i=1;i<=n;++i){
	
		cout<<dis[i]<<" ";
	}
	cout<<endl;
	return 0;
}

Dijkstra的队列合并优化

有时常常涉及取最值修改再放回堆的问题,有时堆是一种浪费
因为它们有隐含的单调性

比如说
最短路dijkstra,如果边权只有\(1\) \(2\) \(3\)
那么可以只开三个队列维护用边权为\(i\)更新的临时最短路的值,每次归并从\(3\)个队列中取最值
可以保证任意时刻三个队列元素都单调不减
当扩展时,取出的距离\(+1\)就入队列\(1\),以此类推

那么如果有\(k\)种边权,就可以开\(k\)个队列

复杂度

\(O((n+m)*w)\)

好一点可以\(O(n*w+m)\)

bellmanford最短路

#include<iostream>
#include<cstdio>
#include<vector>
#include<climits>
#include<cstring>
using namespace std;
struct node{
	int from,to,dis;
	node(int froms,int tos,int diss):from(froms),to(tos),dis(diss){
	}
};
vector<node>edges;
int dis[100009];
int main(){
	memset(dis,0x7f,sizeof dis);
	int n,m,s;
	cin>>n>>m>>s;
	for(int i=0;i<m;++i){
		int a,b,l;
		cin>>a>>b>>l;
		if(a==b)l=0;
		edges.push_back(node(a,b,l)); 
	}
	dis[s]=0;
	
	for(int i=1;i<=n-1;++i){
		int fl=1;//优化,如果这次松弛没有改变边的长度就说明所有最短路都已确定,就可以提前结束循环
		for(int i=0;i<edges.size() ;++i){
			int fr=edges[i].from;
			int to=edges[i].to;
			int diss=edges[i].dis;
			if(dis[fr]+diss<dis[to]){
				dis[to]=dis[fr]+diss;
				fl=0;
			}
		}
		if(fl==1)break;
	}
	for(int i=1;i<=n;++i){
//		if(dis[i]>=1e9){
//			cout<<INT_MAX<<" ";
//			continue;
//		}
		cout<<dis[i]<<" ";
	}
	cout<<endl;
	return 0;
}

负环的定义是:一条边权之和为负数的回路。

johnson算法

是bellman-ford和dijkstra的结合体
是多源最短路算法
是可以应用于负权图,可以判断 负权回路
稀疏图比floyd快
多源最短路要跑n次dijkstra
一次优先队列优化dij最短路\(O((m+n)logm)\)
n次\(O(nlogm*(m+n))\)
平均\(O(n^2 * logn)\)
\(n^3\)floyd快
但是dij不能有负权
所以要解决
如果把每个边都加上一个大数的话就没有负边权了
但是问题来了,从s到t的的每条路径经过的边数不同,所以加上的数的总和也不同,这就会导致原先s到t的最短路径加上k个大数后可能就不会是最短路了
由于最短路径改变了,所以最后s到t加上大数后的最短路-k大数可能不是最短的
举例:
点s到t有两条路径
一条3个边
另一条2个边
3条边的权值总和是14
2条边的权值总和是20
所以最短路是三条边的那个
而每条边都加上10后
3条边的权值总和是44
2条边的权值总和是40
最短路变成了2条边的那个
40-2
10=20
显然20>14,20不是真正最短路径长
所以每个边都加上一个大数不正确

那么怎么办?
首先先建立一个编号为0的点
然后连从0点到每个节点的边,边的权值为0
接下来以0点为源点
用bellman-ford求出编号为i的节点到0点的最短路h[i]
然后给每个边重新设定权值
\(w(s,t)=w(s,t)+h[s]-h[t]\)
接下来跑n次dij
以每个节点为源点(节点0除外)
求出源点到其它点的最短路\(diss(s,t)\)

但这不是真正的最短路\(dis(s,t)\)
\(diss(s,t)=dis(s,t)+h[s]-h[t]\)
所以\(dis(s,t)=diss(s,t)-h[s]+h[t]\)

为什么这样改变边权就可以保证真正最短路且边权非负?
假设边的起点为s,终点为t
原边权为\(w(s,t)\)
新边权为\(ww(s,t)\)
原最短路为\(dis(s,t)\)
新最短路为\(diss(s,t)\)
经过了s,x,y,t四个点
\(ww(s,t)=w(s,t)+h[s]-h[t]\)
\(dis(s,t)=w(s,x)+w(x,y)+w(y,t)\)
\(diss(s,t)=ww(s,x)+ww(x,y)+ww(y,t)\)
\(=w(s,x)+h[s]-h[x]+w(x,y)+h[x]-h[y]+w(y,t)+h[y]-h[t]\)
\(=w(s,x)+w(x,y)+w(y,t)+h[s]-h[t]\)
\(=dis(s,t)+h[s]-h[t]\)
因为\(h[s]-h[t]\)不随最短路边数变化而变化
所以\(diss(s,t)\)与原最短路是同一条路径
\(diss(s,t)\)能保证真正最短路
那为什么边权非负?
\(ww(s,t)=w(s,t)+h[s]-h[t]\)
\(=h[s]+w(s,t)-h[t]\)
因为\(h[t]\)是0到t的最短路
所以其它0到t的路径长度都\(>=h[t]\)
所以\(h[s]+w(s,t)>=h[t]\)
\(h[s]+w(s,t)-h[t]>=0\)
\(ww(s,t)>=0\)
这就保证了边权非负

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
#include<vector>

using namespace std;
#define ll long long
struct edge{
	int dis,to;
	edge(int dis=0,int to=0):dis(dis),to(to){
	}
};
vector<edge>g[3006];
int vis[3006],h[3006];ll dis[3006];
struct path{
	int to;
	ll minpath;
	
	path(int tos=0,ll minpaths=0):to(tos),minpath(minpaths){
	}
	bool operator <(const path&v)const{
		return minpath>v.minpath;
	}
};
priority_queue<path> q;
int main(){
	
	memset(h,127,sizeof h);
//	for(int i=0;i<3006;++i)h[i]=1e18;
	
//	cout<<h[0];
	int n,m;
	cin>>n>>m;
	for(int i=0;i<m;++i){
		int a,b,l;
		cin>>a>>b>>l;
		if(a==b)l=0;//自环 
		g[a].push_back(edge(l,b)); 
	}
	for(int i=1;i<=n;++i){
		g[0].push_back(edge(0,i)); 
	}
	h[0]=0;
	
	for(int i=1;i<=n;++i){//执行n次因为加上0点后有n+1个节点,要松弛n次 
		int fl=1;
		for(int j=0;j<=n;++j){
			for(int k=0;k<g[j].size() ;++k){
				if(h[j]+g[j][k].dis<h[g[j][k].to]){
					h[g[j][k].to]=h[j]+g[j][k].dis;
					fl=0; 
				}
			}
		}
		if(fl)break;//bellmanford优化,路径相等时可以提前结束循环 
	}
	int fl=1;
	for(int j=0;j<=n;++j){
		for(int k=0;k<g[j].size() ;++k){
			if(h[j]+g[j][k].dis<h[g[j][k].to]){
				fl=0;
				break; 
				h[g[j][k].to]=h[j]+g[j][k].dis;}//无论负环和源点是否连通都能判出来 
		}
	}//判断负环, 理论上执行n次后能求出所有点真正最短路,再怎么松弛最短路也不变,但图出现负环时不存在真正最短路,越走越短,所以 可以无限松弛,所以再尝试松弛一次即可判断负环 
	if(!fl){
		cout<<-1<<endl;
		return 0;
	}
	for(int i=0;i<=n;++i){
		for(int j=0;j<g[i].size() ;++j){
			g[i][j].dis=g[i][j].dis +h[i]-h[g[i][j].to];
		}
	}
	for(int i=1;i<=n;++i){
		for(int j=0;j<3006;++j)dis[j]=1e18;
		memset(vis,0,sizeof vis);//每次跑dij都要清空vis并把dis赋值1e18 
		for(int j=0;j<g[i].size() ;++j){
			dis[g[i][j].to]=g[i][j].dis;
		}
		dis[i]=0;
		q.push(path(i,0));
		while(!q.empty() ){
			path k=q.top() ;
			q.pop() ;
			if(vis[k.to ])continue;
			vis[k.to]=1;
			dis[k.to]=k.minpath ;
			for(int j=0;j<g[k.to].size() ;++j){
				if(!vis[g[k.to][j].to]){
					q.push(path(g[k.to ][j].to,dis[k.to]+g[k.to ][j].dis)) ;
				}
			}
		}
		ll sum=0;
		for(int j=1;j<=n;++j){
			ll d=dis[j]-h[i]+h[j];//别忘了加回来 
			if(d>=1e18/2)d=1e9;//不除以2会wa,因为即使源点到点j不连通但由于有负权最终最短路会减去一些量,小于1e18,但>=1e18/2所以必须1e18除以2 
			if(i==j)d=0;//相同点时距离为0 
			
			sum+=j*(d);
		} 
		cout<<sum<<endl;
	}
	return 0;
}

spfa最短路算法

使用有向图(无向图),负边权图
但负权回路存在时无法求出最短路,但可以判断负环
正权环判断要用spfa跑 最长路(越加越大)
spfa最短路过程(无负环)

先初始化dis【】为INF,然后dis【源点】=0
一个队列,s节点入队

开始循环
1.取出队首并出队,将队首入队标记去掉
2.遍历与队首相连的节点,更新dis最短路,同时如果这个节点未入队,就将其入队并做上入队标记
3.重复执行直到队为空

原理:类似于bellman,但是省去了每次对无用节点的遍历,只是将每次路径有变化的节点入队,因为只有这些节点才会使周围节点最短路改变,每次路径无变化的节点可以不用下次再更新它相连的节点

所以队列内放的都是更新过路径,可能引起更新其它路径的节点

为什么要用入队标记?
因为每次找到最短路有变化的节点要入队,但该节点可能在队列中,所以要看是否入队,当节点出队时,该节点的入队标记要去掉,因为后序的松弛可能会更再新这个节点最短路,这个节点会再入队,所以spfa复杂度不稳定,每个节点会入队多次
为什么入队不会造成死循环?
在无负环图中
因为每次更新都只会让一些节点最短路更短,多次更新一定能改变连通的节点的最短路,让其更短,所以最后会找到真正最短路,所以最后最短路不会更新,节点不会再入队,不会造成死循环

有负环时没有真正最短路,只会越来越短,所以spfa会无限循环
此时可以判断是否存在负环
判断是否存在负环
采用spfa的队列法时不能判断松弛次数,因为松弛次数可能会达到n^2,可以判断每个节点的最短路点数
一个文章
在负环图中,若源点与负环不相连,则不会进入死循环,负环中每个点最短路都为INF
若源点与负环相连,则会进入死循环,所以负环中的每个节点最短路都会不断更新,其中每个节点最短路经过点数不断增大
若图没有负环,则一个图的每个点的最短路经过点数最多为n
所以当存在某一个点的最短路经过点数>n时就说明这个点处在与源点相连的负权回路中,可以提前退出循环并提示存在负环

spfa队列法平均\(O(km)\)\(k\)是小常数,\(m\)是边数
最坏\(O(nm)\),\(n\)是点数,\(m\)是边数,即完全图时最坏\(O(n^3)\)

spfa的优化:
slf:队列改为双端队列,每插入节点\(k\)时,把\(dis[k]\)和出队后当前队首\(dis\)(前提队列非空,队列空时插入队尾)比较,若比队首小则插入队首,否则插入队尾(因为距离短的节点更有可能更新到真正最短路,所以应该放在前面,距离大的节点应该放在后面)
lll:用双端队列,记录队列中节点最短路的平均值(记录总和和个数O\((1)\)计算即可),并且每次松弛和出队都要\(O(1)\)更新
如果\(dis[k]<=平均值\)插入队首,否则插入队尾(因为路径更短的更容易引起更新,所以相比平均值小的放在前面,大的放在后面)
带容错的slf:
比较\(dis[k]<dis[front]\)
改为
\(dis[k]+v<dis[front]\)
\(v\)是容错值(常数或随机数或与节点个数和边数有关的数)
这样可以避免陷入局部最优解,使得通过一些故意让临时最小值的点不是真正最短路经过的点的数据

最有效的swap-slf:
当队列改变时将队首和队尾比较当前最短路,若队尾的\(dis\)小于队首\(dis\),则交换队首和队尾
即当每次节点出队或更新最短路时引发了节点入队后,都要比较队首尾的\(dis\)并交换
这样可以使队列接近优先队列,提高效率

mcfx优化:
当节点入队时检查其入队次数,若入队次数在区间\([l,r]\)内插入队首,否则插入队尾
\(l\)\(r\)是常数或与边数和点数有关的数
因为当入队次数小于区间值时并不能确定这个点会引发真正最短路的更新还是数据坑你的点
当入队次数在区间内时这个点很有可能会引发真正最短路更新,所以要插入队首提高效率
当入队次数大于区间时,说明这个节点已经多次更新还没有找到真正最短路,所以这个节点是数据坑你的

边序随机:
避免受输入顺序影响,
所以用vector存完图后要把每个节点相连的边的下标随机打乱一下
用random_shuffle,头文件algorithm
配合srand(time(0))初始随机化种子使用,需要cstdlib和ctime
枚举\(n\)个节点,下标为\(i\)
random_shuffle(g[i].begin(),g[i].end())

差分约束系统:

image
image

对于不等式问题,要转化成图论问题,
由于最短路
可以得到
\(ⅆ_u+W(u,ν)≥ⅆ_ν\)
所以\(d[u]-d[v]>=-w(u,v)\)
\(d[v]-d[u]<=w(u,v)\)
刚好满足题目不等式
所以只需要构建一个图,建立边使得每个点v到点u的边权为不等式右边的值,节点编号为未知数的编号
由于c不等于c',所以没有自环,重边对结果无影响(采用邻接表)
为什么要反向建边?
因为\(d[v]-d[u]<=w(u,v)\)
所以\(d[a]-d[b]<=w(b,a)\)
所以实际上是从b到a的权值,所以要反向建边,让a节点和b节点的边从b到a,权值为不等式右边值
未知数的解是源点到该点的最短路

所以需要最短路,可以用spfa
若存在负环则不存在最短路,所以无解

可是源点呢?
理论上可以取任何点,但是有些点到其它点是单向边,所以最短路会出现正无穷,所以需要新建立一个点,建立一个0点,以该点为起点向所有点连一条边,权值为0,避免最短路的正无穷

差分约束系统的扩展:
不等式\(x_a-x_b>=c\)\(d[b]+w(b,a)>=d[a]\)
\(d[b]-d[a]>=--w(b,a)\)
所以从a到b连一条权值为\(-c\)的边

等式\(x_a-x_b=c\)

分成:\(x_a-x_b>=c\)
\(x_a-x_b<=c\)

的不等式组

分别连从b到a和从a到b的权值为\(c\)\(-c\)的边
(依然满足最短路性质)

对于\(x\)都是整数时
\(x_a-x_b<c\)
可以转化为\(xa-xb<=c-1\)
\(x_a-x_b>c\)
可以为
\(x_a-x_b>=c+1\)

差分约束的性质:

因为\(x_1-x_2<=c\)
\(x_1+d-(x_2+d)=x_1-x_2<=c\)
对于所有不等式组如果左边每个变量都加上同一个实数\(d\),那么会约去这些\(d\),最后不等式组与原不等式组相同
因此对于差分约束系统的任意一组解的每个变量都加上同一个实数\(d\),仍满足差分约束条件,(也就是说一组解可以推出其他解)
dfs的spfa:
从源点出发,将正在访问的点打上正在搜索标记,遍历每一条出去的边,更新最短路,如果最短路 被更新且如果它到达的点没有正在搜索标记就dfs这个点,如果有标记就说明有负环
遍历结束后再把正在访问的点正在搜索标记置为false

队列未判断负环版spfa

#include<iostream>
#include<cstdio>
#include<cstring>
#include<vector>
#include<queue>
#include<deque>
#include<algorithm>
#include<cstdlib>
#include<ctime>
//#include<crandom>
using namespace std;
struct edge{
	int to,dis;
	edge(int to,int dis):to(to),dis(dis){
	}
};
int inqueue[2004];long long dis[2004],num[2004];
vector<edge>g[2004];
int main(){
//	srand(time(0));
	memset(inqueue,0,sizeof inqueue);
	memset(num,0,sizeof num);
	for(int i=0;i<2004;++i){
		dis[i]=6e9;
	}
	int n,m,s;
	cin>>n>>m>>s;
	for(int i=0;i<m;++i){
		int a,b,l;
		cin>>a>>b>>l;
		if(a==b)continue;
		g[a].push_back(edge(b,l)); 
	}
	for(int i=1;i<=n;++i)
//	random_shuffle(g[i].begin() ,g[i].end() );
	dis[s]=0;
	deque<int>q;
	q.push_back(s);
	
	while(!q.empty()){
		
		int k=q.front();
		q.pop_front();
		if(!q.empty() )
//					if(dis[q.front()]>dis[q.back()])swap(q.front(),q.back());
		inqueue[k]=0;
		for(int i=0;i<g[k].size();++i){
			int tos=g[k][i].to;
			int diss=g[k][i].dis;
			if(dis[k]+diss<dis[tos]){
				dis[tos]=diss+dis[k];
				if(!inqueue[tos]){
					q.push_back(tos);
					if(!q.empty() )
					if(dis[q.front()]>dis[q.back()])swap(q.front(),q.back());
					inqueue[tos]=1;
				}
			}
		}
	}
	for(int i=1;i<=n;++i){
//		if(dis[i]>=5e9+30){
//			cout<<(1ll<<31)-1ll<<" ";
//			
//		}else{
			cout<<dis[i]<<" ";
//		}
	}
	
	cout<<endl;
	return 0;
}

队列spfa判断负环:

#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<vector>
#include<algorithm>
#include<queue>
#define MAXN 2004
using namespace std;
struct edge{
	int to,dis;
	edge(int to,int dis):to(to),dis(dis){
	}
};
inline void solve(){
	vector<edge>g[MAXN];
	int inqueue[MAXN],dis[MAXN],num[MAXN];
	memset(inqueue,0,sizeof inqueue);
	memset(dis,0x7f,sizeof inqueue);
	memset(num,0,sizeof num);
	queue<int>q;
	int n,p,m=0,fl=1;
	cin>>n>>p;
	for(int i=0;i<p;++i){
		int u,v,w;
		cin>>u>>v>>w;
		
		if(u==v&&w>=0)continue;//当遇到自环且距离小于0时不能跳过,因为此时构成了负环
		if(u==v&&w<0)fl=0;//但也不能提前退出,因为这样会影响后面的读入,所以要设立标志等读入完成后若被标记就输出yes退出
		g[u].push_back(edge(v,w)); 
		++m;
		if(w>=0){
			g[v].push_back(edge(u,w)); 
			++m;
			
		}
	}
	if(!fl){
		cout<<"YES"<<endl;
		return ;
	}
	q.push(1);
	num[1]=1;//此处将源点最短路经过点数设为1
	dis[1]=0;//别忘了源点初值为0
	while(!q.empty() ){
		int k=q.front();
		q.pop() ;
		inqueue[k]=0;
		for(int i=0;i<g[k].size() ;++i){
			
			int tos=g[k][i].to;
			int diss=g[k][i].dis;
			if(dis[k]+diss<dis[tos]){
				dis[tos]=dis[k]+diss;
				num[tos]=num[k]+1;//当该节点最短路更新时最短路经过节点个数更新为新父节点最短路经过节点个数+1
				
				if(num[tos]>n){//因为之前num[1]=1,所以虽然应该最短路经过点数不包括它自身时大于n-1就为负环,但是由于此处包含了节点自身,所以应该判断>n而不是>=n
					cout<<"YES"<<endl;
					return;
				}
				if(!inqueue[tos]){
					q.push(tos);//别忘了入队
					inqueue[tos]=1; 
				}
			}
		}
	}
//	 for(int i=1;i<=n;++i)cout<<dis[i]<<" ";
	
	cout<<"NO"<<endl;
}

int main(){
	int t;
	cin>>t;
	while(t--){
		solve();
		
	}
	return 0;
}

差分约束系统spfa:

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<vector>
#include<queue>
using namespace std;
#define maxn 5006//至少5002,有超级源点 
struct edge{
	int to,dis;
	edge(int to,int dis):to(to),dis(dis){
	}
};
vector<edge>g[maxn];
int num[maxn],dis[maxn],inqueue[maxn];
int main(){
	memset(inqueue,0,sizeof inqueue);
	memset(num,0,sizeof num);
	memset(dis,0x7f,sizeof dis);
	int n,m;
	cin>>n>>m;
	for(int i=0;i<m;++i){
		int a,b,l;
		cin>>a>>b>>l;
		g[b].push_back(edge(a,l)); //根据上面的讨论,要反向建边
	}
	for(int i=1;i<=n;++i){
		g[0].push_back(edge(i,0)); 
	}
	dis[0]=0;
	num[0]=1;
	queue<int>q;
	q.push(0);
	while(!q.empty()){
		int k=q.front();
		q.pop() ;
		inqueue[k]=0;
		for(int i=0;i<g[k].size() ;++i){
			int tos=g[k][i].to;
			int diss=g[k][i].dis;
			if(dis[k]+diss<dis[tos]){
				dis[tos]=dis[k]+diss;
				num[tos]=num[k]+1;
				if(num[tos]>n+1){//最短路经过节点个数包括其自身大于n+1时才说明有负环,因为加了一个超级源点0点,点数有n+1个
					cout<<"NO"<<endl;
					return 0;
				}
				if(!inqueue[tos]){
					inqueue[tos]=1;
					q.push(tos); 
				}
			}
		}
	}
	for(int i=1;i<=n;++i)cout<<dis[i]<<" ";
	cout<<endl; 
	return 0;
}

dfs版spfa判断负环(最坏指数复杂度):

#include<iostream>
#include<cstdio>
#include<cstring>
#include<vector>
#include<algorithm>
#include<queue>

using namespace std;
#define maxn 2006
struct edge{
	int to,dis;
	edge(int to,int dis):to(to),dis(dis){
	}
};
inline void dfs(int k);
inline void solve();
vector<edge>g[maxn];
int vis[maxn],dis[maxn],lvis=1;
inline void solve(){
	memset(vis,0,sizeof vis);
	memset(dis,0x7f,sizeof dis);
	for(int i=0;i<maxn;++i)g[i].clear();
	int n,p;
	cin>>n>>p;
	int fl=1;
	for(int i=0;i<p;++i){
		int u,v,w;
		cin>>u>>v>>w;
		if(u==v&&w>=0)continue;
		if(u==v&&w<0)fl=0;
		g[u].push_back(edge(v,w)); 
		if(w>=0)g[v].push_back(edge(u,w));
		
	}
	if(!fl){
		cout<<"YES"<<endl;
		return;
	}
	dis[1]=0;
	lvis=1;
	dfs(1);
	if(lvis)
	cout<<"NO"<<endl;
	else{
		cout<<"YES"<<endl;
	}
	
}
inline void dfs(int k){
	if(!lvis)return;
	vis[k]=1;
	for(int i=0;i<g[k].size() ;++i){
		int tos=g[k][i].to;
		int diss=g[k][i].dis;
		
		if(dis[k]+diss<dis[tos]){
			if(!vis[tos]){
				dis[tos]=dis[k]+diss;
				dfs(tos);
			}else{
				lvis=0;//不能直接输出并return,这样会导致多次输出,因为是递归,所以需要标记变量来停止递归
				return;
			}
			
		}
	}
	vis[k]=0;//回溯,这个节点访问完后要去掉访问标记
}

int main(){
	int t;
	cin>>t;
	while(t--){
		solve();
	}
	return 0;
}
posted @ 2026-01-26 13:43  sakrain  阅读(30)  评论(0)    收藏  举报