P3385【模板】负环

【模板】负环

前置知识:掌握了最短路常用的三种算法( Dijkstra,Bellman-Ford,SPFA )

(没掌握也行,只是听着比较困难)

题意

\(T\) 组数据,每组数据给定两个整数 \(n\) (图的顶点数)、 \(m\) (边信息的条数)和 \(m\) 行边信息 \(c\) (起点), \(p\)(终点), \(w\) (边权值)。

输入唯一需要注意点的东西:

\(w \geq 0\) :生成两条有向边 \(c →p\)\(c →p\) (等价于无向边);

\(w < 0\) :只生成一条有向边 \(c →p\) (有向边)。

判断有向图中从顶点 1 出发可达的区域内是否存在负环 ,存在输出 \(YES\) ,不存在则输出 \(NO\)

思路

首先确定一下算法,这题判断是否存在负环,图的话可以考虑最短路,而既然是最短路了,常见的求算法无非就3种:Dijkstra,Bellman-Ford和SPFA,考虑选哪个。

你需要知道的:

Dijkstra:不能处理负边

Bellman-Ford:能处理负边、能判负环;

SPFA:Bellman-Ford 的队列优化版,效率更高。

首先排除Dijkstra,先不说这题目就叫负环,这范围 \(-10^4 \leq w \leq 10^4\) 肯定就给他OUT了。

Bellman-Ford和SPFA之间首选SPFA,毕竟效率更快嘛。

然后就是如何用SPFA来做这题。我们可以在可以在SPFA的过程中搞一个数组 \(cnt_i\) 表示从起点到 \(i\) 点时前经过的节点数,如果该路径没有环,那么他经过的结点数肯定不大于 \(n\) 个,我们可以举个最坏的例子:

当n=4时,最坏的情况就是一条链

负环

(展示画画技术)

当他从起点1到终点4了的时候,前面经过的点最多为4,注意是前面的,也就是到4时只算的是前面经过了的1,2,3。

注意:由于是多组数据,所以记得清空

代码

有注释版

#include <bits/stdc++.h>
using namespace std;
#define ll long long  // 定义long long别名,方便使用
const int N=1e4+5;   // 数组最大容量,根据题目数据范围(n≤2e3),1e4足够

// 全局变量说明:
int n,m;             // n:图的点数,m:边数
int dis[N];          // dis[v]:从起点到v的最短路径长度
int cnt[N];          // cnt[v]:记录v点被松弛的次数(即路径经过的边数)
bool vis[N];         // vis[v]:标记v是否在队列中(避免重复入队)
vector<pair<int,int> >e[N];  // 邻接表存图:e[u]存储(u,v,w),即u到v的边权为w

// 加边函数:向邻接表中添加一条u到v、权值为w的有向边
void add(int u,int v,int w){
	e[u].push_back({v,w});
}

// SPFA算法:检测从起点s出发能到达的负环
// 返回值:true表示存在负环,false表示不存在
bool spfa(int s)
{
	// 初始化:
	memset(vis,0,sizeof vis);        // 所有点初始不在队列中
	memset(dis,0x3f,sizeof dis);    // 最短距离初始化为无穷大(0x3f3f3f3f)
	memset(cnt,0,sizeof cnt);        // 松弛次数初始化为0
	queue<int>q;                     // 存储待松弛节点的队列
	
	// 起点初始化:
	q.push(s);       // 起点入队
	dis[s]=0;        // 起点到自身的距离为0
	
	while (!q.empty())  // 队列非空时持续松弛
	{
		int u=q.front(); q.pop();  // 取出队首节点u并出队
		vis[u]=0;                  // 标记u不在队列中(后续可重新入队)
		
		// 遍历u的所有出边(u->v,权值w)
		for (auto [v,w] : e[u])
		{
			// 松弛操作:如果经过u到v的路径更短,则更新
			if (dis[u]+w<dis[v])
			{
				dis[v]=dis[u]+w;   // 更新v的最短距离
				cnt[v]=cnt[u]+1;   // v的松弛次数 = u的松弛次数 + 1(路径多一条边)
				
				// 负环判定核心:
				// 鸽巢原理:最短路径最多经过n-1条边(无环),若cnt[v]>=n,说明路径中有环
				// 且能持续松弛,说明这个环的总权值为负(负环)
				if (cnt[v]>=n) return 1;
				
				// 如果v不在队列中,入队并标记
				if (!vis[v]) {
					q.push(v);
					vis[v]=1;
				}
			} 
		}
	}
	// 遍历完所有可达节点,未发现负环
	return 0;
}

int main()
{
	int T; cin >>T;  // 输入测试数据组数T
	while (T--)      // 处理每组测试数据
	{
		cin >>n>>m;  // 输入当前测试用例的点数n、边数m
		for (int i=1;i<=m;i++)
		{
			int u,v,w; cin >>u>>v>>w;  // 输入边的起点u、终点v、权值w
			add(u,v,w);                // 添加u->v的有向边
			if (w>=0) add(v,u,w);      // 若w非负,额外添加v->u的有向边(双向边)
		}
		// 调用SPFA检测从1出发的负环,输出结果
		if (spfa(1)) cout <<"YES"<<endl;  // 存在负环输出YES
		else cout <<"NO"<<endl;           // 不存在负环输出NO
		
		// 清空邻接表:为下一组测试数据做准备
		for (int i=1;i<=n;i++) e[i].clear();
	}
	return 0;
}

无注释版

#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int N=1e4+5;
int n,m,dis[N],cnt[N];
bool vis[N];
vector<pair<int,int> >e[N];
void add(int u,int v,int w){e[u].push_back({v,w});}
bool spfa(int s)
{
	memset(vis,0,sizeof vis); memset(dis,0x3f,sizeof dis); memset(cnt,0,sizeof cnt);
	queue<int>q;
	q.push(s);
	dis[s]=0;
	while (!q.empty())
	{
		int u=q.front(); q.pop();
		vis[u]=0;
		for (auto [v,w] : e[u])
		{
			if (dis[u]+w<dis[v])
			{
				dis[v]=dis[u]+w,cnt[v]=cnt[u]+1;
				if (cnt[v]>=n) return 1;
				if (!vis[v]) {q.push(v); vis[v]=1;}
			} 
		}
	}
	return 0;
}
int main()
{
	int T; cin >>T;
	while (T--)
	{
		cin >>n>>m;
		for (int i=1;i<=m;i++)
		{
			int u,v,w; cin >>u>>v>>w;
			add(u,v,w);
			if (w>=0) add(v,u,w);
		}
		if (spfa(1)) cout <<"YES"<<endl;
		else cout <<"NO"<<endl;
		for (int i=1;i<=n;i++) e[i].clear();
	}
	return 0;
}
posted @ 2026-03-21 15:20  头像是美食  阅读(23)  评论(0)    收藏  举报