
在图论中,一个循环(Cycle)是指从图中的某个顶点出发,沿着一系列边最终回到该顶点的路径,且路径上的所有顶点(除了起点和终点)都不重复。在无向图中,由于边是双向的,因此简单地沿着一条边再立即返回不算作循环。循环检测在许多图算法中至关重要,例如判断图是否为树、拓扑排序的先决条件(有向无环图DAG)、最小生成树算法等。
深度优先搜索是一种遍历图的算法,它尽可能深地探索图的分支。在无向图中,DFS可以有效地检测循环,其核心思想是识别“回边”(Back-Edge)。
在DFS遍历过程中,如果从当前顶点 u 访问其邻接点 v 时,发现 v 已经被访问过,并且 v 不是 u 的直接父节点,那么就意味着存在一条从 u 到 v 的边,而 v 已经在当前DFS路径上,这条边 (u, v) 就构成了一个循环。
import java.util.*;
class GraphDFS {
private Map<String, List<String>> adj; // 邻接列表表示图
private Set<String> visited; // 记录已访问的节点
private Map<String, String> parent; // 记录节点的父节点
public GraphDFS(Map<String, List<String>> adjacencyList) {
this.adj = adjacencyList;
}
/**
* 检测无向图中是否存在循环。
* @return 如果存在循环返回 true,否则返回 false。
*/
public boolean hasCycle() {
visited = new HashSet<>();
parent = new HashMap<>();
// 遍历所有节点,确保处理非连通图中的所有组件
// 收集所有可能的节点,包括那些没有邻居的节点
Set<String> allNodes = new HashSet<>(adj.keySet());
for (List<String> neighbors : adj.values()) {
allNodes.addAll(neighbors);
}
for (String node : allNodes) {
if (!visited.contains(node)) {
// 从未访问的节点开始DFS,父节点设为null
if (dfs(node, null)) {
return true; // 发现循环
}
}
}
return false; // 没有发现循环
}
/**
* 深度优先搜索辅助函数。
* @param u 当前访问的节点
* @param p u的父节点
* @return 如果在当前DFS路径中发现循环返回 true,否则返回 false。
*/
private boolean dfs(String u, String p) {
visited.add(u); // 标记当前节点为已访问
parent.put(u, p); // 记录父节点
// 遍历当前节点的所有邻接点
for (String v : adj.getOrDefault(u, Collections.emptyList())) {
if (!visited.contains(v)) {
// 如果邻接点v未被访问,则递归地进行DFS
if (dfs(v, u)) {
return true; // 递归调用发现循环
}
} else if (!v.equals(p)) {
// 如果邻接点v已被访问,且v不是u的父节点,则发现回边,存在循环
return true;
}
}
return false; // 当前路径中未发现循环
}
}并查集(Disjoint Set Union, DSU)是一种用于处理不相交集合的数据结构,它能够高效地执行“查找”(Find)和“合并”(Union)操作。在无向图中,并查集特别适用于检测循环。
并查集将图中的每个顶点视为一个独立的集合。当我们遍历图中的每条边 (u, v) 时:
import java.util.*;
class UnionFind {
private Map<String, String> parent; // 存储每个元素的父节点
private Map<String, Integer> rank; // 存储每个集合的秩(用于按秩合并优化)
public UnionFind(Set<String> nodes) {
parent = new HashMap<>();
rank = new HashMap<>();
for (String node : nodes) {
parent.put(node, node); // 初始时每个节点的父节点是自己
rank.put(node, 0); // 初始秩为0
}
}
/**
* 查找元素i的代表元素(根节点),并进行路径压缩。
* @param i 要查找的元素
* @return 元素i的代表元素
*/
public String find(String i) {
if (!parent.containsKey(i)) {
// 如果节点不在并查集中,可以根据需求处理,例如抛出异常或返回自身
parent.put(i, i);
rank.put(i, 0);
return i;
}
if (parent.get(i).equals(i)) {
return i;
}
String root = find(parent.get(i)); // 递归查找根
parent.put(i, root); // 路径压缩
return root;
}
/**
* 合并元素i和j所在的集合。
* @param i 元素i
* @param j 元素j
* @return 如果成功合并(未形成循环)返回 true,否则返回 false(已在同一集合,形成循环)。
*/
public boolean union(String i, String j) {
String rootI = find(i);
String rootJ = find(j);
if (!rootI.equals(rootJ)) { // 如果不在同一个集合
// 按秩合并优化:将秩较小的树连接到秩较大的树的根上
if (rank.get(rootI) < rank.get(rootJ)) {
parent.put(rootI, rootJ);
} else if (rank.get(rootI) > rank.get(rootJ)) {
parent.put(rootJ, rootI);
} else { // 秩相等时,任意连接,并将新根的秩加1
parent.put(rootJ, rootI);
rank.put(rootI, rank.get(rootI) + 1);
}
return true; // 合并成功,未形成循环
}
return false; // 已经在同一个集合,添加这条边会形成循环
}
}
class GraphUnionFind {
private Map<String, List<String>> adj; // 邻接列表表示图
public GraphUnionFind(Map<String, List<String>> adjacencyList) {
this.adj = adjacencyList;
}
/**
* 检测无向图中是否存在循环。
* @return 如果存在循环返回 true,否则返回 false。
*/
public boolean hasCycle() {
// 收集所有节点,包括那些没有邻居的节点
Set<String> allNodes = new HashSet<>(adj.keySet());
for (List<String> neighbors : adj.values()) {
allNodes.addAll(neighbors);
}
UnionFind uf = new UnionFind(allNodes);
// 遍历所有边。对于无向图,每条边 (u,v) 会在邻接列表中出现两次
// 我们需要确保每条边只被处理一次,以避免重复判断或错误判断
Set<String> processedEdges = new HashSet<>(); // 用于记录已处理的边对,例如 "a-b"
for (String u : adj.keySet()) {
for (String v : adj.getOrDefault(u, Collections.emptyList())) {
// 确保只处理一次边 (u,v),例如通过规范化边表示或跳过已处理的逆向边
String edge以上就是无向图循环检测:深度优先搜索与并查集应用详解的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号