|
| 1 | +/** |
| 2 | + * @file cycle_check_undirected_dsu.cpp |
| 3 | + * @brief Detect cycle in an undirected graph using Disjoint Set Union (Union–Find). |
| 4 | + * |
| 5 | + * @details |
| 6 | + * This implementation uses the Union–Find (Disjoint Set Union) data structure |
| 7 | + * with path compression and union by rank for near-constant-time cycle detection. |
| 8 | + * For each edge (u, v): |
| 9 | + * - If u and v belong to the same set, a cycle exists. |
| 10 | + * - Otherwise, merge their sets. |
| 11 | + * |
| 12 | + * @complexity |
| 13 | + * - Time: O(E α(V)), where α(V) is inverse Ackermann function (~constant) |
| 14 | + * - Space: O(V) |
| 15 | + * |
| 16 | + * @example |
| 17 | + * Input graph: |
| 18 | + * 0--1 |
| 19 | + * | / |
| 20 | + * 2 |
| 21 | + * has a cycle. |
| 22 | + * |
| 23 | +*/ |
| 24 | + |
| 25 | +#include <iostream> |
| 26 | +#include <vector> |
| 27 | +#include <unordered_set> |
| 28 | + |
| 29 | +class DisjointSet { |
| 30 | + private: |
| 31 | + std::vector<int> parent, rank; |
| 32 | + |
| 33 | + public: |
| 34 | + explicit DisjointSet(int n) : parent(n), rank(n, 0) { |
| 35 | + for (int i = 0; i < n; ++i) |
| 36 | + parent[i] = i; |
| 37 | + } |
| 38 | + |
| 39 | + int find(int x) { |
| 40 | + if (parent[x] != x) |
| 41 | + parent[x] = find(parent[x]); |
| 42 | + return parent[x]; |
| 43 | + } |
| 44 | + |
| 45 | + bool unite(int x, int y) { |
| 46 | + int rx = find(x), ry = find(y); |
| 47 | + if (rx == ry) |
| 48 | + return false; // cycle detected |
| 49 | + |
| 50 | + if (rank[rx] < rank[ry]) |
| 51 | + parent[rx] = ry; |
| 52 | + else if (rank[rx] > rank[ry]) |
| 53 | + parent[ry] = rx; |
| 54 | + else { |
| 55 | + parent[ry] = rx; |
| 56 | + rank[rx]++; |
| 57 | + } |
| 58 | + return true; |
| 59 | + } |
| 60 | +}; |
| 61 | + |
| 62 | +/** |
| 63 | + * @brief Detect if an undirected graph contains a cycle. |
| 64 | + * Avoids counting duplicated edges (v,u) and (u,v) twice. |
| 65 | + */ |
| 66 | +bool hasCycle(int V, const std::vector<std::pair<int, int>>& edges) { |
| 67 | + DisjointSet dsu(V); |
| 68 | + std::unordered_set<std::string> seen; |
| 69 | + |
| 70 | + for (const auto& edge : edges) { |
| 71 | + int u = edge.first; |
| 72 | + int v = edge.second; |
| 73 | + |
| 74 | + if (u == v) |
| 75 | + return true; // self-loop is a cycle |
| 76 | + |
| 77 | + // Skip duplicate reversed edges |
| 78 | + std::string key1 = std::to_string(u) + "-" + std::to_string(v); |
| 79 | + std::string key2 = std::to_string(v) + "-" + std::to_string(u); |
| 80 | + if (seen.count(key1) || seen.count(key2)) |
| 81 | + continue; |
| 82 | + seen.insert(key1); |
| 83 | + |
| 84 | + if (!dsu.unite(u, v)) |
| 85 | + return true; // cycle found |
| 86 | + } |
| 87 | + return false; |
| 88 | +} |
| 89 | + |
| 90 | +int main() { |
| 91 | + // Test 1: cyclic triangle |
| 92 | + { |
| 93 | + int V = 3; |
| 94 | + std::vector<std::pair<int, int>> edges = {{0, 1}, {1, 2}, {0, 2}}; |
| 95 | + std::cout << "Test 1 (triangle): " << (hasCycle(V, edges) ? "Cycle\n" : "No cycle\n"); |
| 96 | + } |
| 97 | + |
| 98 | + // Test 2: simple chain |
| 99 | + { |
| 100 | + int V = 4; |
| 101 | + std::vector<std::pair<int, int>> edges = {{0, 1}, {1, 2}, {2, 3}}; |
| 102 | + std::cout << "Test 2 (chain): " << (hasCycle(V, edges) ? "Cycle\n" : "No cycle\n"); |
| 103 | + } |
| 104 | + |
| 105 | + // Test 3: disconnected with one cycle |
| 106 | + { |
| 107 | + int V = 6; |
| 108 | + std::vector<std::pair<int, int>> edges = {{0, 1}, {1, 2}, {2, 0}, {4, 5}}; |
| 109 | + std::cout << "Test 3 (disconnected): " << (hasCycle(V, edges) ? "Cycle\n" : "No cycle\n"); |
| 110 | + } |
| 111 | + |
| 112 | + // Test 4: self-loop |
| 113 | + { |
| 114 | + int V = 3; |
| 115 | + std::vector<std::pair<int, int>> edges = {{0, 1}, {1, 2}, {2, 2}}; |
| 116 | + std::cout << "Test 4 (self-loop): " << (hasCycle(V, edges) ? "Cycle\n" : "No cycle\n"); |
| 117 | + } |
| 118 | + |
| 119 | + return 0; |
| 120 | +} |
0 commit comments