负环问题
负环问题
负环问题(Negative Cycle Problem)是图论中的一个经典问题,指的是在一个有向图中是否存在一条从某个顶点出发,经过若干条边后回到起点的路径。如果存在这样的路径,那么就称这个图存在负环;否则,称这个图不存在负环。
负环问题的解决方法有很多种,其中最常用的是深度优先搜索(DFS)算法。下面我们将详细介绍负环问题的原理及算法流程,并给出相关的C++代码实现。
原理
负环问题的核心思想是:在遍历图的过程中,记录已经访问过的顶点和路径上的边。当遍历到某个顶点时,如果发现该顶点已经被访问过且路径上存在一条从当前顶点出发经过已访问过的顶点的边,那么就可以判断出存在负环。
为了实现这个思路,我们需要使用一个栈来存储待访问的顶点,以及一个集合来存储已经访问过的顶点。在遍历过程中,我们首先将当前顶点压入栈中,然后将其加入已访问顶点的集合。接下来,我们遍历当前顶点的所有邻接顶点,对于每个邻接顶点,如果它尚未被访问过且可以通过当前顶点到达,那么我们就将它压入栈中。最后,当我们再次遇到已访问过的顶点时,就可以判断出存在负环。
算法流程
- 初始化一个空栈stack和一个空集合visited。
- 将起始顶点v压入栈stack中。
- 将起始顶点v加入已访问顶点的集合visited中。
- 当stack非空时,执行以下操作:
a. 弹出栈顶元素u。
b. 如果u已经在visited集合中,说明找到了负环,结束算法。
c. 否则,将u的所有未访问过的邻居顶点压入栈stack中,并将它们加入visited集合中。 - 如果算法执行到这里,说明没有找到负环。
下面给出负环问题C++代码实现:
#include<bits/stdc++.h> // 包含常用的C++库
#define reg register // 定义一个宏,表示使用寄存器变量
using namespace std; // 使用标准命名空间
// 读取输入的整数
inline int read(){
int x=0,f=1;
char ch=getchar();
while(!isdigit(ch)){
if(ch=='-') f=-1; // 如果输入的是负号,将f设为-1
ch=getchar();
}
while(isdigit(ch)){
x=(x<<1)+(x<<3)+(ch^48); // 将输入的数字转换为十进制
ch=getchar();
}
return x*f; // 返回结果,如果输入的是负数,返回负值
}
// 输出整数
void write(int x){
if(x<0){
putchar('-');
x=-x; // 如果输入的是负数,先输出负号,再取反
}
if(x>9) write(x/10); // 如果输入的数字大于9,递归调用write函数处理十位数和个位数
putchar(x%10+'0'); // 输出个位数
return ;
}
const int MAXN=2003,MAXM=3003,inf=INT_MAX; // 定义常量
struct e{ // 定义结构体,表示图的边
int to,w,nextt; // 边的终点、权重和下一个节点在链表中的位置
};
vector<e > edge; // 存储图的边的向量
int t,n,m,head[MAXN],dis[MAXN],cnt[MAXN]; // 定义图的顶点数、边数、头指针数组、距离数组和计数数组
bool vis[MAXN]; // 定义标记数组,表示是否访问过某个节点
// 初始化图的数据结构
void init(){
edge.clear(); // 清空边向量
memset(head,-1,sizeof(head)); // 将头指针数组的所有元素设为-1
fill(dis+1,dis+n+1,inf); // 将距离数组的所有元素设为无穷大
memset(vis,0,sizeof(vis)); // 将标记数组的所有元素设为0
memset(cnt,0,sizeof(cnt)); // 将计数数组的所有元素设为0
return ;
}
// 在图中添加一条边
void add_edge(int u, int v, int w){
edge.push_back((e){v,w,head[u]}); // 在边向量中添加一条新的边,起点为u,终点为v,权重为w,下一个节点在头指针数组中的下标为head[u]
head[u]=edge.size()-1; // 将头指针数组中u对应的元素设为新添加的边的下标
}
// 实现最短路径算法SPFA(Shortest Path Faster Algorithm)
bool spfa(){
dis[1]=0,vis[1]=1; // 将起点的距离设为0,标记为已访问
queue<int > q; // 创建一个队列q用于BFS遍历
q.push(1); // 将起点加入队列q中
while(!q.empty()){ // 当队列q不为空时循环执行以下操作
int x=q.front(); // 从队列q的前面取出一个元素作为当前节点x
q.pop(); // 将当前节点x从队列q中移除
vis[x]=0; // 将当前节点x的标记设为未访问过的状态
for(reg int i=head[x];i!=-1;i=edge[i].nextt){ // 遍历当前节点x的所有邻接点y和它们的边z和权重w
int y=edge[i].to,z=edge[i].w; // 将邻接点y和边的权重w分别赋值给y和w
if(dis[y]>dis[x]+z){ // 如果从起点到邻接点y的路径长度比从起点到邻接点x的路径长度加上边的权重w更长
dis[y]=dis[x]+z; // 则更新从起点到邻接点y的路径长度为从起点到邻接点x的路径长度加上边的权重w
cnt[y]=cnt[x]+1; // 并将从起点到邻接点y的路径上经过的边的数量加一
if(cnt[y]>=n) return 1; // 如果从起点到邻接点y的路径上经过的边的数量等于n(即所有的顶点都已经访问过),则返回true表示存在负权回路
if(!vis[y]){ // 如果邻接点y还没有被访问过
q.push(y); // 则将邻接点y加入队列q中等待访问
vis[y]=1; // 并将邻接点的标记设为已访问过的状态
}
}
}
}
return 0; // 如果没有找到负权回路,则返回false表示不存在负权回路
}
int main(){ // 主要函数开始执行的地方,程序的入口函数
scanf("%d",&t); // 从标准输入读取一个整数t作为测试用例的数量
edge.push_back((e){0,0,0});
while(t--){
scanf("%d%d",&n,&m);
init();
for(reg int i=1;i<=m;i++){
// int u=read(),v=read(),w=read();
int u,v,w;
scanf("%d%d%d",&u,&v,&w);
add_edge(u,v,w);
if(w>=0) add_edge(v,u,w);
}
if(spfa()) printf("YES\n");
else printf("NO\n");
}
return 0;
}
例题及题解
例题:给定一个有向图的邻接表表示,判断该图是否存在负环。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 如何使用 Uni-app 实现视频聊天(源码,支持安卓、iOS)
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)