当前位置: 首页 > news >正文

无向图的回路检测(广度优先并查集)

  • 广度优先——父节点数组法

  • 核心思路

无环图的任意连通分量本质是一棵树,任选其中一个节点作为根,BFS就相当于对这棵树做层序遍历 。遍历需借助队列,先将根节点入队,出队时检查当前节点cur的所有邻居

遇到邻居节点时分三种情况:一是邻居未访问,就把cur设为它的父节点,标记访问后入队;二是邻居已访问且是cur的父节点,这是树的正常双向边,符合树的结构,无需处理

三是邻居已访问但不是cur的父节点,此时会出现“root→cur→邻居”和“root→邻居父节点→邻居”两条路径 ,违背了树中两节点路径唯一的特性,由此可判定图中存在回路

  • 邻接矩阵

bool bfs_hascycle(int prime,int** adj,int n,bool visited[],int parent[]){visited[prime]=true;EnQueue(Q,prime);while(!EmptyQueue(Q)){int cur=DeQueue(Q);for(int i=0;i<n;i++){if(adj[cur][i]){int cur_neighbor=i;if(visited[cur_neighbor]){if(parent[cur]!=cur_neighbor){return true;}}else{parent[cur_neighbor]=cur;EnQueue(Q,cur_neighbor);}}}}return false;
}bool Hascycle(int** adj,int n){bool* visited=(bool *)malloc(n*sizeof(bool));memset(visited,0,n*sizeof(bool));int *parent=(int *)malloc(n*sizeof(int));memset(parent,-1,n*sizeof(int));bool res=false;for(int i=0;i<n;i++){if(!visited[i]){res=bfs_hascycle(i,adj,n,visited,parent);if(res) break;}}free(visited);free(parent);return res;
}
  • 邻接表(数组)

bool bfs_hascycle(int prime,int** adj,int adjColSize[],bool visited[],int parent[]){visited[prime]=true;EnQueue(Q,prime);while(!EmptyQueue(Q)){int cur=DeQueue(Q);for(int i=0;i<adjColSize[cur];i++){int cur_neighbor=adj[cur][i];if(visited[cur_neighbor]){if(parent[cur]!=cur_neighbor){return true;}}else{parent[cur_neighbor]=cur;EnQueue(Q,cur_neighbor);}}}return false;
}bool Hascycle(int** adj,int n,int adjColSize[]){bool *visited=(bool*)malloc(n*sizeof(bool));memset(visited,0,n*sizeof(bool));int *parent=(int*)malloc(n*sizeof(int));memset(parent,-1,n*sizeof(int));bool res=false;for(int i=0;i<n;i++){if(!visited[i]){res=bfs_hascycle(i,adj,adjColSize,visited,parent);if(res) break;}}free(visited);free(parent);return res;
}
  • 邻接表(链表)

bool bfs_hascycle(int prime,LinkNode** adj,bool visited[],int parent[]){visited[prime]=true;EnQueue(Q,prime);while(!EmptyQueue(Q)){int cur=DeQueue(Q);LinkNode *p=adj[cur];while(p){int cur_neighbor=p->data;if(visited[cur_neighbor]){if(parent[cur]!=cur_neighbor){return true;}}else{parent[cur_neighbor]=cur;EnQueue(Q,cur_neighbor);}p=p->next;}}return false;
}bool Hascycle(LinkNode** adj,int n){bool *visited=(bool*)malloc(n*sizeof(bool));memset(visited,0,n*sizeof(bool));int *parent=(int*)malloc(n*sizeof(int));memset(parent,-1,n*sizeof(int));bool res=false;for(int i=0;i<n;i++){if(!visited[i]){res=bfs_hascycle(i,adj,visited,parent);if(res) break;}}free(visited);free(parent);return res;
}
  • 广度优先——为何与“三状态数组法”不适配

三状态法是专门为深度优先搜索设计的。在 DFS 中,每个节点从被访问到回溯的整个过程都有明确的阶段:

未访问(0)、正在访问(1)、访问结束(2)。这种状态的变化依赖于递归调用栈,因此当在搜索过程中遇到一个状态为“正在访问”的节点时,就能判断出存在一条回到当前递归路径上的回边,也就是环路。

然而广度优先搜索的遍历方式完全不同。BFS 使用队列按层推进,不会像 DFS 那样形成递归路径,也不会出现回溯的阶段。节点一旦入队并被访问,就立即标记为已访问;它不会在访问过程中维持“中间状态”。

因此,在 BFS 中,节点的访问状态只有“访问过”与“未访问”两种。三状态法失去了它的意义,也就无法再用来判断是否存在回到当前搜索路径的回边。在这种情况下,BFS 通常使用“父节点法”来替代三状态法检测环路。


  • 并查集

  • 核心思路

并查集检测无向图回路的关键前提是避免边的重复处理 ,因无向图的边在邻接矩阵、邻接表中均为双向存储(同一条边存两次),需针对不同存储结构采用差异化去重方案:

邻接矩阵:聚焦三角区遍历

借助邻接矩阵 adj[i][j] = adj[j][i] 的对称性,通过设置循环条件(如 j ≥ i 遍历上三角区,或 j ≤ i 遍历下三角区),直接锁定每条边仅被处理一次,无需额外标记,高效规避重复 。

邻接表:依托节点编号判断

无论数组邻接表还是链式邻接表,遍历过程中需通过节点编号大小关系去重 。因边的两个端点编号固定,只需在处理时判断“后一节点编号大于前一节点编号”(或反之),仅执行一次并查集的“查根-合并”操作,即可彻底避免同一条边的重复计算 。

  • 邻接矩阵

#define Max 1000
int parent[Max];
int rank[Max];
void Initialize(int n){for(int i=0;i<n;i++){parent[i]=i;rank[i]=1;}
}
int find(int x){if(parent[x]!=x){parent[x]=find(parent[x]);}return parent[x];
}
void UnionSet(int x,int y){int root_x=find(x);int root_y=find(y);if(root_x==root_y) return;if(rank[root_x]<rank[root_y]){parent[root_x]=root_y;}else if(rank[root_x]>rank[root_y]){parent[root_y]=root_x;}else{parent[root_x]=root_y;rank[root_y]++;}
}bool Hascycle(int** adj,int n){Initialize(n);for(int i=0;i<n;i++){for(int j=i;j<n;j++){if(adj[i][j]){int find_i=find(i);int find_j=find(j);if(find_i==find_j) return true;else{UnionSet(find_i,find_j);}}}}return false;
}
  • 邻接表(数组)

#define Max 1000
int parent[Max];
int rank[Max];
void Initialize(int n){for(int i=0;i<n;i++){parent[i]=i;rank[i]=1;}
}int find(int x){if(parent[x]!=x){parent[x]=find(parent[x]);}return parent[x];
}void UnionSet(int x,int y){int root_x=find(x);int root_y=find(y);if(root_x==root_y) return;if(rank[root_x]<rank[root_y]){parent[root_x]=root_y;}else if(rank[root_x]>rank[root_y]){parent[root_y]=root_x;}else{parent[root_x]=root_y;rank[root_y]++;}
}bool Hascycle(int** adj,int n,int adjColSize[]){Initialize(n);for(int i=0;i<n;i++){for(int j=0;j<adjColSize[i];j++){int cur=i;int cur_neighbor=adj[i][j];if(cur<cur_neighbor){int find_cur=find(cur);int find_cur_neighbor=find(cur_neighbor);if(find_cur==find_cur_neighbor) return true;else{UnionSet(find_cur,find_cur_neighbor);}}}}return false;
}
  • 邻接表(链表)

#define Max 1000
int parent[Max];
int rank[Max];
void Initialize(int n){for(int i=0;i<n;i++){parent[i]=i;rank[i]=1;}
}int find(int x){if(parent[x]!=x){parent[x]=find(parent[x]);}return parent[x];
}void UnionSet(int x,int y){int root_x=find(x);int root_y=find(y);if(root_x==root_y) return;if(rank[root_x]<rank[root_y]){parent[root_x]=root_y;}else if(rank[root_x]>rank[root_y]){parent[root_y]=root_x;}else{parent[root_x]=root_y;rank[root_y]++;}
}bool Hascycle(LinkNode** adj,int n){Initialize(n);for(int i=0;i<n;i++){LinkNode *p=adj[i];while(p){int cur=i;int cur_neighbor=p->data;if(cur<cur_neighbor){int find_cur=find(cur);int find_cur_neighbor=find(cur_neighbor);if(find_cur==find_cur_neighbor) return true;else{UnionSet(find_cur,find_cur_neighbor);}}p=p->next;}}    return false;
}

  • 无向图的回路检测常用的四种方法

无向图的回路检测常用的四种方法:基于深度优先搜索(DFS)的两种变体、基于广度优先搜索(BFS)的方法,以及基于并查集(Union-Find)的结构。这四种方法虽然目标一致,但思路、实现方式和效率特点各不相同。

首先,从整体思想上看,DFS 与 BFS 属于图的遍历类方法,它们通过在搜索过程中动态维护“访问状态”和“父节点”来判断是否存在环。而并查集属于集合合并类方法,它不需要遍历整张图的所有路径,而是通过集合的合并和查找关系来快速判断连通性冲突。这使得前两类方法更适合图的结构性分析,而并查集更擅长在边的动态加入过程中判断是否出现环。

在深度优先搜索的第一种方法中,我们通常使用一个二状态数组(未访问 / 已访问)以及一个父节点数组。搜索时,每次访问一个节点,就标记它为已访问,然后继续访问它的邻居。如果发现某个邻居已经被访问过,且这个邻居不是当前节点的父节点,就说明出现了回路。这种方法直观、简单、易实现,但因为只有两种状态,它依赖父节点信息来排除“返回父节点”的情况,否则会出现误判。

第二种深度优先方法采用了一个三状态数组来替代父节点数组。通常定义 0 为未访问,1 为正在访问(路径上的节点),2 为已访问完成(递归回溯后的节点)。在递归过程中,如果从当前节点访问到某个状态为 1 的节点,说明在同一条搜索路径中出现了回到前面的情况,这就构成了一个环。而父节点标记仅用于辅助判断哪些回溯属于真正的回路、哪些只是从路径中返回。这种方法逻辑上更清晰,不必频繁依赖父节点数组来判断关系,尤其在递归实现中更自然。

相比之下,广度优先搜索的环检测方式思路类似 DFS 的第一种,但它是层次式的。我们同样使用一个二状态数组来记录访问情况,并配合一个父节点数组。当从某个节点出发依次遍历它的邻居时,如果发现一个邻居已经访问过,且该邻居不是当前节点的父节点,也意味着存在环。BFS 版本的优势在于不依赖递归,适合处理节点数量很大、层次结构较宽的图结构,但其逻辑实现较 DFS 更为冗长,尤其在维护父节点时更复杂一些。

最后是并查集方法。它不基于遍历,而是基于集合的动态合并。初始时,每个节点自成一个集合。对于图中的每一条边 (u, v),我们查找它们各自的根节点。如果根相同,说明这两个顶点已经在同一个连通分量中,再加入这条边就会形成环。若根不同,则合并这两个集合。通过路径压缩与按秩合并优化后,并查集的查询和合并几乎是常数时间,因此在边数较多或需要多次动态判断时效率非常高。它尤其适合稀疏图、或者需要频繁判断“新增边是否成环”的场景。

总体来看,DFS 与 BFS 方法更偏重“搜索过程中的局部结构判断”,可以明确指出回路所在路径;而并查集方法则更偏向“整体连通性判断”,能快速判断环是否存在,却不容易直接获得环的具体组成。若图较小或需要找出具体回路,DFS 较为直观;若图较大或只关心是否有环,并查集则更高效。四种方法各有适用场景,但它们共同揭示了一个思想核心:无向图的回路实质上是不同路径间连通关系的冲突。

http://www.dtcms.com/a/457607.html

相关文章:

  • 磁悬浮轴承损耗:深度解析损耗机理与降耗之道
  • AI大模型赋能药物研发:破解“双十困局”的跨界革命
  • 哲林高拍仪网站开发宁波南部商务区网站建设
  • 经典的逻辑函数化简算法 Espresso
  • ZKEACMS:基于ASP.Net Core开发的开源免费内容管理系统
  • 【QT常用技术讲解】opencv实现摄像头图像检测并裁剪物体
  • 深圳建网站哪个好网页设计实训总结3000字大学篇
  • 【密码学实战】openHiTLS mac命令行:消息认证码工具
  • 沙井网站建设石林县工程建设个体交易网站
  • Chromium 138 编译指南 - Android 篇:安装构建依赖项(七)
  • asp 绿色环保企业网站源码 v1.1golang 做网站
  • 第4章 C++多线程系统编程精要
  • 绵阳网站怎样做网站设计要交税吗
  • 同ip网站有什么影响网站建设论文500字
  • 基于springboot的家具商城销售系统
  • 手机版网站制作佛山建站软件
  • 厦门南希网站建设微信应用程序开发
  • 【STM32项目开源】基于STM32的智能厨房火灾燃气监控
  • 最新彩虹云商城二开Pro美化版 新增超多功能 全开源
  • 如何制作家具网站东莞设计网站建设方案
  • 商丘做网站的电话怎样建网站最快
  • 安徽住房与城乡建设部网站网站登记备案 个人
  • 嵌入式开发--温度、湿度、气压传感器BME280
  • 基于ZYNQ FPGA+AI+ARM 的卷积神经网络加速器设计
  • 自助建站之星wordpress图片弹出
  • JavaScript-防抖与节流
  • 广西南宁网站建设哪家好网站调用微信数据
  • 专业手机网站建设公司排名wordpress the7打开速度慢
  • 做网站的主要作用设计师培训学费
  • 【大模型】DeepSeek-V3.2-Exp中的DSA稀疏注意力设计