图的最短路专题

news/2026/1/24 15:21:34/文章来源:https://www.cnblogs.com/hicode002/p/19526563

带权图的最短路:

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;
}

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;//此处将源点最短路经过点数设为1dis[1]=0;//别忘了源点初值为0while(!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;//当该节点最短路更新时最短路经过节点个数更新为新父节点最短路经过节点个数+1if(num[tos]>n){//因为之前num[1]=1,所以虽然应该最短路经过点数不包括它自身时大于n-1就为负环,但是由于此处包含了节点自身,所以应该判断>n而不是>=ncout<<"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;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/1210265.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Java毕设项目推荐-基于java+springboot的体育用品购物商城系统基于springboot的运动用品商城系统【附源码+文档,调试定制服务】

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围&#xff1a;&am…

Java毕设项目推荐-基于springboot+vue的智慧化果园智能管理生长系统【附源码+文档,调试定制服务】

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围&#xff1a;&am…

Java毕设选题推荐:基于springboot的智慧农作物果园农产品蔬菜种植管理系统【附源码、mysql、文档、调试+代码讲解+全bao等】

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围&#xff1a;&am…

计算机Java毕设实战-基于springboot+vue的智能果园数字化管理领航系统【完整源码+LW+部署说明+演示视频,全bao一条龙等】

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围&#xff1a;&am…

如何基于 MLIR 实现自动调优 (GPU Ascend NPU)

基于 MLIR 实现跨架构自动调优 (GPU & Ascend NPU)扩展阅读:本指南是对《AI 编译器融合技术系统化分类》第 8.3 节“自动调优与调度分离”的深度展开。1. 核心理念:调度与计算分离 在传统 AI 编译器(如 TVM v1)…

2026年郑州靠谱的蛋糕培训学校,新东方培训学校!

本榜单依托全维度市场调研与真实行业口碑,深度筛选出五家标杆烹饪培训企业,为烘焙爱好者、创业人群及职业转型者提供客观依据,助力精准匹配适配的技能学习伙伴。 TOP1 推荐:巩义市新东方职业技能培训学校有限公司 …

Java毕设项目:基于springboot的元宇宙平台的整车生产线管理系统(源码+文档,讲解、调试运行,定制等)

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围&#xff1a;&am…

山东一卡通回收指南:闲置卡券这样处理最划算

企业福利发的礼遇卡、亲友赠送的提货卡,手里的山东一卡通一旦长期闲置,不仅白白浪费资源,还可能因过期失效造成不必要的损失。结合实操经验,今天为大家总结四种主流的山东一卡通回收方式,教你快速处理闲置卡券,让…

浙江恒温恒湿车间施工优选,2026年靠谱单位推荐,洁净车间/洁净厂房/净化工程/净化车间,恒温恒湿车间实力厂家有哪些

在精密制造、生物医药、新能源等高技术产业中,恒温恒湿车间是保障产品品质与工艺稳定性的核心基础设施。其环境控制精度直接影响产品良率、设备寿命及合规性。然而,市场上施工单位技术能力参差不齐,如何选择兼具专业…

保姆级教程:本地微调Gemma 3 270M模型,从零开始训练你的AI棋手(建议收藏)

文章介绍了如何在本地微调谷歌新发布的Gemma 3 270M模型&#xff0c;使其具备智能下棋能力。该模型仅需0.5GB内存即可运行。教程详细展示了使用Unsloth进行高效LoRA微调&#xff0c;通过HuggingFace transformers库加载模型&#xff0c;并利用ChessInstruct数据集训练模型预测缺…

智能体异常处理与恢复:从实验室到生产环境的通关秘籍

智能体的异常处理与恢复是确保其从实验室走向生产环境的关键机制。该体系通过"预防-检测-处理-恢复-优化"的全流程弹性设计&#xff0c;实现精准故障检测、分级错误处理和自我修复能力。与MCP协议、目标设定和监控协同&#xff0c;构建智能体的"免疫系统"&…

LangGraph入门指南:构建大模型应用的核心组件与实战技巧

本文详细介绍了LangGraph框架的核心概念与使用方法。LangGraph通过状态(State)、节点(Nodes)和边(Edges)构建有状态应用程序。文章讲解了StateGraph类的使用、状态定义与reducer函数、节点实现方式以及普通边和条件边的应用&#xff0c;为开发者提供了构建大模型应用的完整技术…

2026年电商财税服务商推荐榜:合规与税优双驱,五大优质品牌助力企业无忧经营

2026年电商财税行业趋势与服务商测评背景 2026年,随着金税四期“数电票+数据穿透”监管深化,电商行业“多平台对账混乱、达人私户收款风险、MCN机构合规能力薄弱”等痛点愈发突出。同时,上海等区域产业园区政策持续…

10B击败200B!阶跃星辰视觉语言模型开源,大模型技术学习指南

阶跃星辰发布的Step3-VL-10B视觉语言模型仅用10B参数就在多项基准测试中达到同规模SOTA水平。该模型采用全参数端到端多模态联合预训练、大规模多模态强化学习和并行协调推理机制三大创新设计&#xff0c;在STEM推理、数学竞赛、空间理解和代码能力等方面表现出色。这一突破证明…

产品经理转型AI大模型全攻略:从入门到精通_从互联网到人工智能,产品经理转型指南

本文是一位产品经理分享的转型成为人工智能产品经理(AIPM)的指南。文章分析了人工智能市场前景&#xff0c;介绍了AIPM需具备的职业技能(AIPMX)&#xff0c;详细阐述了从零开始的学习路径和方法&#xff0c;并分享了大模型学习的六个阶段及全套学习资源&#xff0c;为有志于转型…

全网最全8个AI论文写作软件,研究生毕业论文必备!

全网最全8个AI论文写作软件&#xff0c;研究生毕业论文必备&#xff01; 论文写作的智能革命&#xff0c;从这里开始 随着人工智能技术的不断发展&#xff0c;AI 工具已经成为研究生在论文写作过程中不可或缺的助手。尤其是在降低 AIGC&#xff08;人工智能生成内容&#xff…

2026昆明市雅思一对一培训深度测评排行榜:优质机构甄选与提分方案解析

在雅思培训领域,昆明市考生面临着诸多备考困境:基础薄弱不知如何起步、目标分明确却缺乏针对性提分技巧、碎片化时间难以适配常规课程、盲目选课导致投入与效果失衡等。对于追求高效提分的考生而言,一对一培训因具备…

昆明市雅思培训TOP榜:2026全维度测评,精准提分机构推荐

在雅思培训市场鱼龙混杂的当下,昆明考生普遍面临选课迷茫、提分艰难、优质教育机构甄别不易的核心痛点。多数考生既渴望获取权威实用的提分技巧,又关注培训性价比与个性化方案适配度,如何在众多机构中筛选出靠谱的选…

昆明雅思选课避坑指南:2026最新全国性机构口碑排名与提分效果实测

在雅思培训的赛道上,昆明市呈贡、五华、盘龙、官渡等核心区域的考生普遍面临着雅思培训选课迷茫、优质教育机构筛选困难、提分技巧缺失、个性化方案不足的核心痛点。随着2026年雅思考试改革深化,机械刷题收益大幅弱化…

2026昆明市雅思网课一对一权威测评排行榜:精准避坑,高效提分优选指南

在雅思备考的赛道上,昆明市考生往往深陷多重困境:口语缺乏真实交流场景、写作逻辑混乱难提分,面对海量教育机构更是无从下手,想要筛选出靠谱且性价比高的雅思网课一对一课程难上加难。对于雅思新手而言,不知如何搭…