【spfa 判负环】P3385 【模板】负环

在这里插入图片描述

分析

主要是如何在spfa里判负环的问题
首先易得,对于一个不存在负环的图,从起点到任意一个点最短距离经过的点最多只有 n 个
而spfa里,在没有负环的情况下,一个点到另一个点的最短路最多被其他所有点更新,所以若某个点入队大于n次后说明有负环
当然,若spfa要判断正环,将最短路换为最长路即可,判断准则仍然是入队n次

同时注意输出

code

#include<bits/stdc++.h>
using namespace std;
#define loop(i,start,end) for(register int i=start;i<=end;++i)
#define clean(arry,num); memset(arry,num,sizeof(arry));
const int maxn=2000+10;
const int maxm=3000+10;
int n,m;
int T;
int cnt=0;
struct node
{
	int e;int w;int nxt;
}e[maxm<<1];
int head[maxn];
int vis[maxn];
int dis[maxn];
inline int read()
{
	int ans=0;bool neg=false;char r=getchar();
	while(r>'9'||r<'0'){if(r=='-')neg=true;r=getchar();}
	while(r>='0'&&r<='9'){ans=ans*10+r-'0';r=getchar();}
	return (neg)?-ans:ans;
}
inline void addl(int u,int v,int w)
{
	e[cnt].e=v;
	e[cnt].w=w;
	e[cnt].nxt=head[u];
	head[u]=cnt;
	++cnt;
}
inline void spfa()
{
	clean(vis,0);
	clean(dis,0x3f);
	queue<int>q;
	q.push(1);
	vis[1]++;
	dis[1]=0;
	while(q.empty()==false)
	{
		int f=q.front();q.pop();
		for(int i=head[f];i!=-1;i=e[i].nxt)
		{
			int v=e[i].e;
			if(dis[v]>dis[f]+e[i].w)
			{
				dis[v]=dis[f]+e[i].w;
				q.push(v);
				vis[v]++;
				if(vis[v]>n)
				{
					printf("YE5\n");
					return;
				}
			}
		}
	}
	printf("N0\n");
}
int main()
{
	#ifndef ONLINE_JUDGE
    freopen("datain.txt","r",stdin);
    #endif
    T=read();
    while(T--)
    {
    	cnt=0;
		clean(head,-1);
    	n=read(),m=read();
    	loop(i,1,m)
    	{
    		int a,b,w;
    		a=read(),b=read(),w=read();
    		addl(a,b,w);
    		if(w>=0)addl(b,a,w);
		}
		spfa();
	}
	return 0;
}

update 2019.8.16

找负环不用SLF优化的原因

考虑如下图(菊花图):

在这里插入图片描述

绿色的边边权比其他边小的多,但实际上负环在黄色的边上

这就是这道题为什么不加优化的spfa反而可以过的原因

<think>好的,我现在需要解决用户关于P3385模板的问题,使用C++中的Bellman-Ford算法。首先,我得回忆一下Bellman-Ford算法的基本思想步骤,以及如何用它来检测。 Bellman-Ford算法主要用于求解单源最短路径,特别是处理有权边的情况。它的核心思想是对所有边进行松弛操作,重复V-1次(V是顶点数),因为最短路径最多包含V-1条边。如果在V-1次之后还能继续松弛,说明存在。 但是,用户的问题是关于如何检测模板题。这时候,我需要考虑标准的Bellman-Ford算法是否足够高效,或者是否需要优化。因为标准的Bellman-Ford的时间复杂度是O(VE),对于较大的图来说可能不够快,尤其是像竞赛题目中的情况,可能会有较大的数据量。比如题目中的顶点数可能到几千,边数更多的话,可能会导致超时。 这时候,可能需要考虑使用队列优化的SPFA算法(Shortest Path Faster Algorithm),它实际上是Bellman-Ford的一种优化版本,通过队列来避免不必要的松弛操作。SPFA的平均时间复杂度较低,但在最坏情况下仍为O(VE)。不过对于检测SPFA通常更高效。 但用户明确提到了Bellman算法,所以可能需要先给出Bellman-Ford的标准实现,然后讨论可能的优化或者变种。不过在实际应用中,SPFA更常用于检测,所以可能需要解释两者之间的关系,并说明SPFA是基于Bellman-Ford的改进。 接下来,我需要回忆如何用Bellman-Ford检测。标准做法是进行V-1次松弛后,再进行一次松弛,如果还能松弛,则存在。或者另一种方式是,对每个顶点进行松弛,如果在第V次迭代中仍有顶点的距离被更新,则存在。 但可能存在更高效的方式,比如使用一个数组记录每个顶点被松弛的次数,如果在某次松弛中,某个顶点的松弛次数超过V-1次,那么说明存在。因为最短路径最多有V-1条边,超过的话说明进入了。 现在考虑具体实现。对于Bellman-Ford的标准实现,每次遍历所有边进行松弛。但这样的时间复杂度较高,所以可能需要优化。例如,使用邻接表存储图结构,每次迭代遍历所有边,并在V次迭代后检查是否有更新。 或者,使用SPFA的方式,维护一个队列,记录可能被松弛的顶点。同时,记录每个顶点入队的次数,如果某个顶点的入队次数超过V次,则存在。这是因为在SPFA中,一个顶点最多被松弛V次(在没有的情况下,每个顶点的松弛次数不超过V-1次)。 现在需要确定题目中的输入格式。根据P3385的题目描述,输入可能包含多个测试用例,每个用例给出顶点数n,边数m,然后是m条边的信息。边可能是有向的或者无向的,需要注意处理。例如,如果是无向边,则相当于添加两条有向边。但如果有权边的情况下,无向边可能导致。例如,如果一条无向边的权值为数,那么来回走这条边就会形成,并且总权值不断减少,所以存在。 所以,在构建图的时候,对于无向边需要拆分成两个方向的有向边。这可能影响到边的数量,例如,边数m可能被处理成2m,如果所有边都是无向的话。但具体要看题目中的输入格式。比如题目可能给出的边是u到v的有权边,如果是无向的话,需要手动添加反向边。 现在,针对Bellman-Ford的标准实现,代码的大致结构是: 初始化距离数组dist为INF,源点dist[1]=0(假设源点是1)。 进行n-1次松弛操作,每次遍历所有边,检查是否可以松弛。 然后,再进行第n次松弛,如果发现可以松弛,则存在。 但是,这样的方法是否正确?比如,当图中有多个连通分量时,源点所在的连通分量可能没有,而其他连通分量可能存在。这时候,仅以源点作为起点可能无法检测到其他连通分量的。因此,正确的做法可能需要从每个顶点出发进行检查,或者确保源点所在的连通分量是否存在。这可能影响解题的正确性。 例如,题目中的测试用例可能包含多个连通分量,而可能存在于其他连通分量中。此时,如果仅以某个源点(如1)作为起点,可能无法检测到。这时候,需要如何处理? 因此,正确的做法可能需要将所有顶点的初始距离设为0,然后进行一次松弛操作。或者,可能需要为每个顶点作为起点运行Bellman-Ford,但这样时间复杂度会非常高,无法通过题目时间限制。 因此,这可能意味着题目中的图是连通的,或者题目假设必须从源点可达。或者,可能存在更高效的处理方式,例如,在初始时将所有顶点加入队列,并设置距离为0,这样SPFA可以检测整个图中的,不管起点在哪里。 例如,在SPFA中,初始将所有顶点入队,并将距离设为0。这样,每个顶点都会被尝试作为起点,如果存在任何,都会被检测到。因为即使某个顶点不在所在的连通分量中,但如果存在,那么该中的顶点会被不断松弛,从而被检测到。 所以,这时候可能需要采用SPFA算法,并且将所有顶点初始入队,这样更有效地检测整个图中的。 回到用户的问题,用户要求的是用Bellman算法,但可能对于这个问题来说,必须使用SPFA才能高效通过。因此,在回答时需要说明Bellman-Ford的标准方法可能不够高效,而SPFA更适合,并且给出相应的实现。 因此,我需要组织回答的结构: 1. 问题分析:的定义及检测方法。 2. Bellman-Ford算法的原理,及其检测的方式。 3. 标准Bellman-Ford的实现可能存在的问题,如时间复杂度高。 4. 引入SPFA作为优化,解释其如何工作,如何检测。 5. 具体实现步骤,包括数据结构的选择(邻接表)、队列的使用、松弛次数统计等。 6. 代码实现,处理无向边的情况,注意每条无向边需要拆分为两条有向边。 7. 测试样例的注意事项,例如处理多个测试用例,初始化相关变量。 同时,需要注意题目中的输入输出格式。例如,输入可能包含多组数据,每组数据给出nm,然后是m条边。对于每条边,如果是无向的,则添加两条有向边。在代码中,处理时需要根据输入是否为无向边来决定是否添加反向边。 例如,题目中的输入可能给出边的情况,比如u到v的权值为w的有向边,或者无向边。例如,题目中的输入可能有无向边的标记,例如,当输入的是无向边时,需要将u→vv→u都加入到邻接表中,权值相同。 但需要根据题目具体描述确定。假设题目中的输入边是无向的,那么在代码中需要处理为两条有向边。但根据题目编号P3385,查看题目描述的话,可能实际输入中的边是有向的,但可能存在需要处理的情况,比如当题目给出的是无向边时,是否添加两条边?比如,题目中的情况可能包括这样的情况:给出的边是双向的,所以需要拆分成两条边,这可能形成。 例如,当给出边u-v的权值为数时,那么这两个方向的边会形成一个,来回走的话,总权值会不断减少,形成。例如,如果边权是-2,那么u→v→u的权值之是-4,再走一次是-6,等等,每次总权值更小,因此存在。 因此,在代码中处理无向边时,必须将每个无向边拆分为两个有向边。否则可能无法正确检测到。 现在,针对代码的编写步骤: 1. 定义邻接表的结构,例如使用结构体或pair来存储边。 2. 处理输入,将每条边存入邻接表。如果是无向边,则添加两次。 3. 实现SPFA算法: a. 初始化距离数组为INF,或者某些初始值。或者,根据某些策略,比如初始将所有顶点的距离设为0,并加入队列,这样可以检测整个图中的。 b. 维护一个队列,保存需要松弛的顶点。 c. 维护一个数组cnt,记录每个顶点的入队次数,或者松弛次数。如果某个顶点的cnt超过n,说明存在。 d. 每次取出队首顶点u,遍历其所有邻接边,尝试松弛。如果成功,则将v加入队列,并更新cnt[v]。如果cnt[v]超过n,返回存在。 4. 需要注意,在SPFA中,传统的做法是从某个起点开始,但为了检测整个图中的,可能需要将所有顶点初始入队。或者,可能存在某些顶点无法从源点到达的情况,所以初始将所有顶点入队,距离设为0,这样可以保证任何都会被检测到。否则,如果所在的连通分量中没有顶点被初始加入队列,那么可能无法检测到。 例如,假设初始时只有顶点1入队,而存在于另一个连通分量中,那么SPFA可能无法处理到该。因此,正确的做法是初始化所有顶点到队列,并将它们的距离设为0。这样,每个顶点都被视为可能的起点,任何中的顶点都会被松弛多次。 因此,代码中的初始化步骤应该是: dist数组初始化为某个值,比如0或者INF。这里如果初始化为0,那么当存在权边时,可以更快地触发松弛。例如,假设顶点u的dist[u]为0,而存在边u→v的权值为w,如果w是数,那么dist[v]可以被更新为0 + w,这将导致v被加入队列,进而继续松弛其他顶点。 这样,即使存在于图的任何位置,只要初始将所有顶点入队,就能被检测到。这可能是一种常用的技巧,用于检测整个图中的,而不仅仅是某个源点可达的部分。 现在,关于SPFA的实现: - 需要一个队列,用来保存待处理的顶点。 - visited数组,用来记录顶点是否在队列中,避免重复入队。 - cnt数组,记录每个顶点的松弛次数。 代码的大致结构: bool hasNegativeCycle(int n) { queue<int> q; vector<int> dist(n+1, 0); // 初始化为0,更容易触发松弛 vector<int> cnt(n+1, 0); vector<bool> inqueue(n+1, true); // 初始所有顶点都在队列中 for(int i=1; i<=n; ++i) { q.push(i); } while(!q.empty()) { int u = q.front(); q.pop(); inqueue[u] = false; for(auto &edge : adj[u]) { int v = edge.v; int w = edge.w; if(dist[v] > dist[u] + w) { dist[v] = dist[u] + w; if(!inqueue[v]) { q.push(v); inqueue[v] = true; cnt[v]++; if(cnt[v] > n) { return true; } } } } } return false; } 这样,当某个顶点被松弛超过n次时,就说明存在。因为正常情况下,顶点最多被松弛n-1次(最短路径最多经过n-1条边),超过n次则存在。 需要注意,这里的初始距离设为0,而传统的SPFA可能将源点距离设为0,其他设为INF。但在这里,为了检测所有可能的,初始所有顶点的距离设为0,这样任何可以通过权边减少的路径都会被检测到。 例如,假设有一个,那么中的某个顶点u的初始距离是0,中的边u→v的权值w为数,那么v的距离会被更新为更小,导致进入队列,接着继续松弛中的其他顶点,最终导致cnt超过n次。 现在,回到具体的题目实现: 题目中的输入可能有多个测试用例。每个测试用例中,顶点数n,边数m。然后m行,每行给出u, v, w,可能还有一个参数表示是否是无向边。或者,根据题目描述,可能边是无向的,例如题目中的边可能是无向的,所以需要拆分成两条有向边。 例如,题目描述可能说明该题中的边是无向的,所以在处理输入时,需要将每条边u-v的权值w,拆分为u→vv→u两条边,权值都为w。 例如,在代码中,读取每条边的时候: for(int i=0; i<m; ++i) { int u, v, w; cin >> u >> v >> w; adj[u].push_back(Edge(v, w)); if(w >= 0) { // 如果是无向边且权值非,或者题目中的边是无向的 adj[v].push_back(Edge(u, w)); } // 或者,如果题目中边是无向的,并且权值可以是数: adj[v].push_back(Edge(u, w)); } 但需要根据题目描述确认。例如,P3385的题目描述中,边可能是有向的还是无向的? 查阅题目描述:P3385模板的题目中,输入格式为,第一行一个整数T,表示测试数据组数。对于每组测试数据,第一行两个整数N,M,表示点数边数。接下来M行,每行三个整数u,v,w,表示存在一条从u到v的有向边,权值为w。如果w≥0,则表示还存在一条从v到u的边,权值为w。否则,仅存在一条从u到v的有向边。 或者可能题目中的边都是无向的?需要确认题目描述。假设题目中的边是无向的,当w>=0时,表示无向边,否则为有向边。或者可能原题中的输入边是否有方向? 例如,原题中的输入可能给出的是有向边,但题目需要检测图中是否存在,不管边是否有向。这时候,可能需要将每条边视为有向边处理。或者,原题中的边可能有些是无向的,有些是有向的,这需要根据输入的参数断。 例如,题目可能说明,当w >=0时,边是无向的,否则是有向的。这种情况下,当输入w为数时,仅添加u→v的有向边;如果w非,则添加u→vv→u两条边。 所以,在处理输入时,需要根据w的正来决定是否添加反向边。例如: 当读入u, v, w后,将u→v的边加入邻接表。如果w >=0,则还需要加入v→u的边。 这样,在代码中: for(int i=0; i<m; ++i) { int u, v, w; cin >> u >> v >> w; adj[u].emplace_back(v, w); if(w >= 0) { adj[v].emplace_back(u, w); } } 这样,当w为数时,只添加一条有向边,否则添加两条,形成无向边。这可能就是题目中的输入规则,这会影响图中是否存在的可能性。例如,当w为数时,该边是单向的,否则是双向的。 这种情况下,当添加的边中,可能存在,例如,两个顶点之间的双向边权值都为,则形成一个,可以构成。 现在,针对代码的编写,需要注意邻接表的构建方式,以及如何处理输入。 综上,最终的代码结构可能如下: - 处理多组测试数据。 - 每组数据中,读取nm。 - 初始化邻接表。 - 读取m条边,根据w的正添加边。 - 使用SPFA算法检测。 - 输出结果。 现在,代码中的一些细节需要注意: - 邻接表的类型:可能用vector<vector<Edge>> adj(n+1)。 - Edge结构体包含目标顶点v权值w。 - 初始化时,dist数组设为0,所有顶点入队。 - cnt数组记录入队次数,当超过n次时返回存在。 - 注意处理队列中的顶点,避免重复入队,使用inqueue数组。 现在,测试该算法是否正确。例如,假设存在一个三角形,三个顶点,边权均为-1,则每个顶点的松弛次数会增加,直到超过n次,从而被检测到。 例如,顶点A→B权-1,B→C权-1,C→A权-1。这将形成一个,总权-3。每次绕一周,距离会更小,因此会不断被松弛,导致cnt超过n次。 综上,这样的代码应该能够正确检测。现在,将这些思路转化为具体的代码步骤,并给出正确的实现。</think>### 问题分析 是指图中权值数的路。检测的常用方法是基于Bellman-Ford算法的松弛思想。由于标准Bellman-Ford复杂度较高($O(VE)$),实际应用中常用其优化版本SPFA(Shortest Path Faster Algorithm)。 --- ### 算法核心思路 1. **Bellman-Ford原理** 通过$V-1$轮松弛操作遍历所有边,若第$V$轮仍能松弛,说明存在。 2. **SPFA优化** - 用队列维护待松弛的顶点,避免无效松弛。 - 统计每个顶点的入队次数。若某个顶点入队次数超过$V$次,则存在。 --- ### 代码实现步骤 1. **邻接表存图** 根据输入构建邻接表。若边权非,则添加双向边;否则仅添加单向边。 2. **SPFA检测** - 初始化所有顶点入队,距离设为0。 - 松弛过程中统计入队次数,若超过顶点数$n$,定存在。 --- ### 完整代码(C++) ```cpp #include <iostream> #include <vector> #include <queue> using namespace std; struct Edge { int v, w; Edge(int _v, int _w) : v(_v), w(_w) {} }; bool hasNegativeCycle(int n, const vector<vector<Edge>>& adj) { vector<int> dist(n + 1, 0); // 初始距离设为0 vector<int> cnt(n + 1, 0); // 入队次数统计 vector<bool> inqueue(n + 1, true); // 标记是否在队列中 queue<int> q; // 初始所有顶点入队 for (int i = 1; i <= n; i++) { q.push(i); } while (!q.empty()) { int u = q.front(); q.pop(); inqueue[u] = false; for (const Edge& e : adj[u]) { int v = e.v; int w = e.w; if (dist[v] > dist[u] + w) { // 尝试松弛 dist[v] = dist[u] + w; if (!inqueue[v]) { q.push(v); inqueue[v] = true; cnt[v]++; if (cnt[v] > n) { // 入队次数超过n,存在 return true; } } } } } return false; } int main() { ios::sync_with_stdio(false); cin.tie(0); int T; cin >> T; while (T--) { int n, m; cin >> n >> m; vector<vector<Edge>> adj(n + 1); for (int i = 0; i < m; i++) { int u, v, w; cin >> u >> v >> w; adj[u].emplace_back(v, w); if (w >= 0) { // 非权值,添加反向边 adj[v].emplace_back(u, w); } } if (hasNegativeCycle(n, adj)) { cout << "YES\n"; } else { cout << "NO\n"; } } return 0; } ``` --- ### 关键点解析 1. **输入处理** - 若边权$w \ge 0$,添加双向边;否则仅单向。 - 通过邻接表高效存储图结构。 2. **SPFA优化逻辑** - 所有顶点初始入队,确保检测所有连通分量的。 - 入队次数超过$n$直接(路径长度超过$n-1$必存在)。 3. **时间复杂度** - 平均$O(E)$,最坏$O(VE)$,适用于稀疏图。 --- ### 测试样例 输入: ``` 1 3 3 1 2 2 2 3 2 3 1 -5 ``` 输出: ``` YES ``` 解释:顶点1→2→3→1构成路权值为$2+2-5 = -1 < 0$,存在
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

AndrewMe8211

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值