Union-find (disjoint sets)
Track which items belong to the same group as groups merge, with a parent array, union by rank and path compression, in almost O(1) per operation.
Two operations
Find(x): which group is x in? Returns a representative item (the group’s “root”). Two items are in the same group if and only if they have the same root. Union(x, y): merge the groups of x and y into one.
parent[i] is the item above i, and a root is its own parent. Find walks up the parents to the root. Union finds both roots and makes one the parent of the other: one array write.
Two tricks that make it fast
Union by rank (or size): attach the shorter tree under the taller one, never the other way round. Trees then stay at most log₂ n tall. Path compression: after Find walks up to the root, point every item on that path directly at the root. The next Find for any of them is one step.
var beams = new DisjointSet(10);int[][] bolts = { new[] { 1, 2 }, new[] { 3, 4 }, new[] { 2, 4 }, new[] { 5, 6 }, new[] { 7, 8 }, new[] { 6, 8 } };foreach (var b in bolts) beams.Union(b[0], b[1]); Console.WriteLine($"Assemblies: {beams.Count}");Console.WriteLine($"1 and 3 connected? {beams.Connected(1, 3)}");Console.WriteLine($"1 and 5 connected? {beams.Connected(1, 5)}");Console.WriteLine($"Union(4, 4) merged anything? {beams.Union(4, 4)}"); class DisjointSet{ private readonly int[] _parent, _rank; public int Count { get; private set; } // number of separate groups public DisjointSet(int n) { _parent = new int[n]; _rank = new int[n]; for (int i = 0; i < n; i++) _parent[i] = i; // everyone starts alone Count = n; } public int Find(int x) { if (_parent[x] != x) _parent[x] = Find(_parent[x]); // path compression return _parent[x]; } public bool Union(int a, int b) { int ra = Find(a), rb = Find(b); if (ra == rb) return false; // already in the same group if (_rank[ra] < _rank[rb]) (ra, rb) = (rb, ra); _parent[rb] = ra; // shorter tree under the taller one if (_rank[ra] == _rank[rb]) _rank[ra]++; Count--; return true; } public bool Connected(int a, int b) => Find(a) == Find(b);}Assemblies: 4 1 and 3 connected? True 1 and 5 connected? False Union(4, 4) merged anything? False
Union returning false is a useful signal on its own: it means the two items were already connected, so this new connection closes a loop.
Where union-find is used
Kruskal’s MST: “are the ends of this edge already connected?” Cycle detection in undirected graphs as edges arrive. Dynamic connectivity: network links coming up one by one, “are these two computers connected yet?” Grouping equal things: merging duplicate customer records that share an email or phone number, or labelling connected regions in an image.
Show a hint
Union returns false.
Show the solution
int[][] links = { new[] { 1, 2 }, new[] { 2, 3 }, new[] { 3, 4 }, new[] { 1, 4 }, new[] { 1, 5 } };var loop = FirstLoop(5, links);Console.WriteLine(loop is null ? "no loop" : $"({loop[0]}, {loop[1]})"); static int[]? FirstLoop(int n, int[][] links){ var ds = new DisjointSet(n + 1); foreach (var link in links) if (!ds.Union(link[0], link[1])) return link; // already connected: this closes a loop return null;} class DisjointSet{ private readonly int[] _parent, _rank; public DisjointSet(int n) { _parent = new int[n]; _rank = new int[n]; for (int i = 0; i < n; i++) _parent[i] = i; } public int Find(int x) => _parent[x] == x ? x : _parent[x] = Find(_parent[x]); public bool Union(int a, int b) { int ra = Find(a), rb = Find(b); if (ra == rb) return false; if (_rank[ra] < _rank[rb]) (ra, rb) = (rb, ra); _parent[rb] = ra; if (_rank[ra] == _rank[rb]) _rank[ra]++; return true; }}(1, 4)
Remember
Union-find tracks merging groups with one parent array; a root represents its group. Find walks up to the root; Union links one root under the other. Union by rank plus path compression make both operations practically O(1). Used by Kruskal, cycle detection, dynamic connectivity and record merging.