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

浙公网安备 33010602011771号