Union-Find
Track which items are connected as links arrive, merging groups and answering same-group questions in effectively constant time.
How It Works
Union-find (also called a disjoint set union, or DSU) tracks how a collection of elements splits into groups that never overlap. It supports two operations: find(x) returns a representative for the group holding x, and union(a, b) merges the groups holding a and b. Two elements are connected exactly when find returns the same representative for both. A BFS or DFS from Graph Traversal can answer that once, but union-find keeps up while edges keep arriving, without walking the graph again.
Each group is a tree of parent pointers, stored in a single parent array. A root points to itself and stands for its group. find follows pointers up to the root, and union finds both roots and points one at the other. Done naively, a run of unions can build one long chain and make find O(n). Two small changes prevent that.
index: 0 1 2 3 4 5
parent: [0, 0, 0, 3, 3, 5]
0 3 5
/ \ |
1 2 4
sets: {0, 1, 2} {3, 4} {5}- Union by size (or by rank): always hang the smaller tree under the root of the bigger one. An element only gets deeper when its tree joins one at least as big, so the size of its tree at least doubles each time. That can happen at most log n times, so no tree is ever taller than log n.
- Path compression: once
findreaches the root, point every element it passed straight at that root. The next lookup for any of them takes one step.
before find(4): 4 -> 3 -> 2 -> 1 -> 0
after find(4): 4 -> 0
3 -> 0
2 -> 0
1 -> 0Union by Size and Path Compression
Step 1 of 10
forest
- Pointer or count just changed
- Path find walks, roots being compared
- Already in the same set
- Size at a non-root, no longer used
Six elements, each its own root: parent[i] = i, every size is 1 and count is 6. A root is any element that is its own parent.
All 10 steps as text
- Six elements, each its own root:
parent[i] = i, everysizeis1andcountis6. A root is any element that is its own parent. union(0, 1):0and1are both roots and their sizes tie at1, so1goes under0.countdrops to5.union(1, 2):find(1)walks up to0and2is its own root.size[0] = 2beatssize[2] = 1, so root2goes under root0.2hangs under the root0, not under1, so it sits one step from the root.size[0]grows to3andcountdrops to4.union(3, 4):3and4are both roots and their sizes tie at1, so4goes under3.countdrops to3.union(2, 0):find(2)andfind(0)both return0. They're already one set, sounionreturnsfalseandcountstays3.union(2, 4):find(2)walks up to0andfind(4)walks up to3.size[0] = 3beatssize[3] = 2, so root3goes under root0.parent[3] = 0moves all of{3, 4}at once, because4still points at3. Nowsize[0] = 5andcount = 2.find(4)walks4 -> 3 -> 0.4is 2 steps from its root, the deepest any element has been, because its whole tree just moved under0.- Path compression: on the way back,
findpoints4straight at root0, so the nextfind(4)takes one step.countis still2, since only pointers moved.
With both tricks, any sequence of m operations on n elements takes O(m α(n)) time, where α is the inverse Ackermann function. The Ackermann function grows faster than any tower of exponents you could write down, so its inverse grows slower than anything you'll ever meet: α(n) is at most 4 for any n that could fit in a computer, or in the universe. So in practice each operation costs a small constant, averaged over the sequence. Either trick alone still gives O(log n) per operation (amortized, in the case of path compression).
When to Reach for It
- Connections arrive one at a time, and you need to know whether two items are already linked.
- You need the number of connected groups, especially as groups keep merging.
- You need the edge that creates a cycle in an undirected graph.
- Items are equivalent through a chain of facts (a shared email, a shared row or column,
a == b) and need grouping. - Kruskal's minimum spanning tree asks "are these two already connected?" once per edge.
Complexity
| Operation | Time | Space |
|---|---|---|
| find or union, both optimizations | O(α(n)) amortized, effectively O(1) | O(1), or O(log n) stack if find is recursive |
| find or union, union by size only | O(log n) | O(1) |
| find or union, no optimizations | O(n) worst case | O(1) |
| Set up n elements | O(n) | O(n) |
The Template
A Disjoint Set Union
The class keeps three things. parent is the pointer array. size counts the elements in each group, and only its entries at roots are kept up to date. count is the number of separate groups: it starts at n and drops by one each time union really merges two groups.
find is recursive. It finds the root through its parent, then points itself straight at that root on the way back out, which is path compression in two lines. The recursion is only as deep as the tree, and union by size keeps that at most log n.
union returns whether it merged anything, and that return value is often the whole answer. false means a and b were already connected, so in a graph the edge between them closes a cycle.
Time O(α(n)) amortized per find or unionSpace O(n)
class DisjointSet:
"""Disjoint sets over the elements 0..n-1, with path compression and union by size."""
def __init__(self, n: int) -> None:
self.parent = list(range(n)) # Every element starts as its own root.
self.size = [1] * n # Only meaningful at a root: how many elements its set holds.
self.count = n # How many separate sets there are.
def find(self, x: int) -> int:
"""The root that represents x's set."""
if self.parent[x] != x:
# Path compression: point x straight at the root for next time.
self.parent[x] = self.find(self.parent[x])
return self.parent[x]
def union(self, a: int, b: int) -> bool:
"""Merge the sets holding a and b. Returns False if they were already one set."""
root_a, root_b = self.find(a), self.find(b)
if root_a == root_b:
return False
# Union by size: hang the smaller tree under the bigger one's root.
if self.size[root_a] < self.size[root_b]:
root_a, root_b = root_b, root_a
self.parent[root_b] = root_a
self.size[root_a] += self.size[root_b]
self.count -= 1
return True
if __name__ == "__main__":
ds = DisjointSet(6)
for a, b in [(0, 1), (1, 2), (3, 4)]:
ds.union(a, b)
print(ds.count) # 3: {0, 1, 2}, {3, 4} and {5}
print(ds.find(0) == ds.find(2)) # True
print(ds.find(0) == ds.find(3)) # False
print(ds.union(2, 0)) # False: already in the same set
print(ds.union(2, 4)) # True
print(ds.count) # 2// Disjoint sets over the elements 0..n-1, with path compression and union by size.
export class DisjointSet {
private parent: number[];
private size: number[]; // Only meaningful at a root: how many elements its set holds.
count: number; // How many separate sets there are.
constructor(n: number) {
this.parent = Array.from({ length: n }, (_, i) => i); // Every element starts as its own root.
this.size = new Array<number>(n).fill(1);
this.count = n;
}
// The root that represents x's set.
find(x: number): number {
if (this.parent[x] !== x) {
// Path compression: point x straight at the root for next time.
this.parent[x] = this.find(this.parent[x]);
}
return this.parent[x];
}
// Merge the sets holding a and b. Returns false if they were already one set.
union(a: number, b: number): boolean {
let rootA = this.find(a);
let rootB = this.find(b);
if (rootA === rootB) return false;
// Union by size: hang the smaller tree under the bigger one's root.
if (this.size[rootA] < this.size[rootB]) [rootA, rootB] = [rootB, rootA];
this.parent[rootB] = rootA;
this.size[rootA] += this.size[rootB];
this.count--;
return true;
}
}
const ds = new DisjointSet(6);
for (const [a, b] of [[0, 1], [1, 2], [3, 4]]) ds.union(a, b);
console.log(ds.count); // 3: {0, 1, 2}, {3, 4} and {5}
console.log(ds.find(0) === ds.find(2)); // true
console.log(ds.find(0) === ds.find(3)); // false
console.log(ds.union(2, 0)); // false: already in the same set
console.log(ds.union(2, 4)); // true
console.log(ds.count); // 2package main
import "fmt"
// disjointSet holds disjoint sets over the elements 0..n-1, with path
// compression and union by size.
type disjointSet struct {
parent []int
size []int // Only meaningful at a root: how many elements its set holds.
count int // How many separate sets there are.
}
func newDisjointSet(n int) *disjointSet {
ds := &disjointSet{parent: make([]int, n), size: make([]int, n), count: n}
for i := range n {
ds.parent[i] = i // Every element starts as its own root.
ds.size[i] = 1
}
return ds
}
// find returns the root that represents x's set.
func (ds *disjointSet) find(x int) int {
if ds.parent[x] != x {
// Path compression: point x straight at the root for next time.
ds.parent[x] = ds.find(ds.parent[x])
}
return ds.parent[x]
}
// union merges the sets holding a and b. It returns false if they were
// already one set.
func (ds *disjointSet) union(a, b int) bool {
rootA, rootB := ds.find(a), ds.find(b)
if rootA == rootB {
return false
}
// Union by size: hang the smaller tree under the bigger one's root.
if ds.size[rootA] < ds.size[rootB] {
rootA, rootB = rootB, rootA
}
ds.parent[rootB] = rootA
ds.size[rootA] += ds.size[rootB]
ds.count--
return true
}
func main() {
ds := newDisjointSet(6)
for _, e := range [][2]int{{0, 1}, {1, 2}, {3, 4}} {
ds.union(e[0], e[1])
}
fmt.Println(ds.count) // 3: {0, 1, 2}, {3, 4} and {5}
fmt.Println(ds.find(0) == ds.find(2)) // true
fmt.Println(ds.find(0) == ds.find(3)) // false
fmt.Println(ds.union(2, 0)) // false: already in the same set
fmt.Println(ds.union(2, 4)) // true
fmt.Println(ds.count) // 2
}using System;
public class Solution
{
// Disjoint sets over the elements 0..n-1, with path compression and union by size.
public class DisjointSet
{
readonly int[] parent;
readonly int[] size; // Only meaningful at a root: how many elements its set holds.
public int Count { get; private set; } // How many separate sets there are.
public DisjointSet(int n)
{
parent = new int[n];
size = new int[n];
for (int i = 0; i < n; i++)
{
parent[i] = i; // Every element starts as its own root.
size[i] = 1;
}
Count = n;
}
// The root that represents x's set.
public int Find(int x)
{
if (parent[x] != x)
{
// Path compression: point x straight at the root for next time.
parent[x] = Find(parent[x]);
}
return parent[x];
}
// Merge the sets holding a and b. Returns false if they were already one set.
public bool Union(int a, int b)
{
int rootA = Find(a), rootB = Find(b);
if (rootA == rootB) return false;
// Union by size: hang the smaller tree under the bigger one's root.
if (size[rootA] < size[rootB]) (rootA, rootB) = (rootB, rootA);
parent[rootB] = rootA;
size[rootA] += size[rootB];
Count--;
return true;
}
}
public static void Main()
{
var ds = new DisjointSet(6);
foreach (var (a, b) in new[] { (0, 1), (1, 2), (3, 4) }) ds.Union(a, b);
Console.WriteLine(ds.Count); // 3: {0, 1, 2}, {3, 4} and {5}
Console.WriteLine(ds.Find(0) == ds.Find(2)); // True
Console.WriteLine(ds.Find(0) == ds.Find(3)); // False
Console.WriteLine(ds.Union(2, 0)); // False: already in the same set
Console.WriteLine(ds.Union(2, 4)); // True
Console.WriteLine(ds.Count); // 2
}
}public class Solution {
// Disjoint sets over the elements 0..n-1, with path compression and union by size.
static class DisjointSet {
private final int[] parent;
private final int[] size; // Only meaningful at a root: how many elements its set holds.
int count; // How many separate sets there are.
DisjointSet(int n) {
parent = new int[n];
size = new int[n];
for (int i = 0; i < n; i++) {
parent[i] = i; // Every element starts as its own root.
size[i] = 1;
}
count = n;
}
// The root that represents x's set.
int find(int x) {
if (parent[x] != x) {
// Path compression: point x straight at the root for next time.
parent[x] = find(parent[x]);
}
return parent[x];
}
// Merge the sets holding a and b. Returns false if they were already one set.
boolean union(int a, int b) {
int rootA = find(a), rootB = find(b);
if (rootA == rootB) return false;
// Union by size: hang the smaller tree under the bigger one's root.
if (size[rootA] < size[rootB]) {
int swap = rootA;
rootA = rootB;
rootB = swap;
}
parent[rootB] = rootA;
size[rootA] += size[rootB];
count--;
return true;
}
}
public static void main(String[] args) {
DisjointSet ds = new DisjointSet(6);
for (int[] e : new int[][] {{0, 1}, {1, 2}, {3, 4}}) ds.union(e[0], e[1]);
System.out.println(ds.count); // 3: {0, 1, 2}, {3, 4} and {5}
System.out.println(ds.find(0) == ds.find(2)); // true
System.out.println(ds.find(0) == ds.find(3)); // false
System.out.println(ds.union(2, 0)); // false: already in the same set
System.out.println(ds.union(2, 4)); // true
System.out.println(ds.count); // 2
}
}Worked Problems
Each solution stays folded until you open it, so you can try the problem first. Every one is a complete program in Python, TypeScript, Go, C# and Java, run and checked against its expected output.
Problem
There are n cities. You get an n x n matrix isConnected where isConnected[i][j] is 1 if a road runs directly between cities i and j, and 0 if not. A province is a group of cities linked to each other directly or through other cities, with no road to any city outside the group. Return the number of provinces.
Example: [[1, 1, 0], [1, 1, 0], [0, 0, 1]] returns 2. Cities 0 and 1 form one province, and city 2 is a province on its own.
Show SolutionHide Solution
Approach
Start with every city as its own province, so count = n. Then union the two ends of every road. A union that joins two different groups lowers the count by one, and a road inside an existing group changes nothing. Once every road is processed, count is the answer.
The matrix is symmetric, since a road from i to j is also a road from j to i, and the diagonal only says each city reaches itself. So the loop reads just the upper triangle, j > i.
Time O(n² α(n)), effectively O(n²) to read the matrixSpace O(n)
def find_circle_num(is_connected: list[list[int]]) -> int:
"""How many groups of cities are linked, directly or through other cities."""
n = len(is_connected)
ds = DisjointSet(n) # Every city starts as its own province.
for i in range(n):
# The matrix is symmetric, so the upper triangle has every road once.
for j in range(i + 1, n):
if is_connected[i][j] == 1:
ds.union(i, j)
return ds.count
class DisjointSet:
"""The disjoint set from the template: path compression plus union by size."""
def __init__(self, n: int) -> None:
self.parent = list(range(n))
self.size = [1] * n
self.count = n
def find(self, x: int) -> int:
if self.parent[x] != x:
self.parent[x] = self.find(self.parent[x])
return self.parent[x]
def union(self, a: int, b: int) -> bool:
root_a, root_b = self.find(a), self.find(b)
if root_a == root_b:
return False
if self.size[root_a] < self.size[root_b]:
root_a, root_b = root_b, root_a
self.parent[root_b] = root_a
self.size[root_a] += self.size[root_b]
self.count -= 1 # Two provinces just became one.
return True
if __name__ == "__main__":
print(find_circle_num([[1, 1, 0], [1, 1, 0], [0, 0, 1]])) # 2
print(find_circle_num([[1, 0, 0], [0, 1, 0], [0, 0, 1]])) # 3
print(find_circle_num([[1, 0, 0, 1], [0, 1, 1, 0], [0, 1, 1, 1], [1, 0, 1, 1]])) # 1// How many groups of cities are linked, directly or through other cities.
export function findCircleNum(isConnected: number[][]): number {
const n = isConnected.length;
const ds = new DisjointSet(n); // Every city starts as its own province.
for (let i = 0; i < n; i++) {
// The matrix is symmetric, so the upper triangle has every road once.
for (let j = i + 1; j < n; j++) {
if (isConnected[i][j] === 1) ds.union(i, j);
}
}
return ds.count;
}
// The disjoint set from the template: path compression plus union by size.
class DisjointSet {
private parent: number[];
private size: number[];
count: number;
constructor(n: number) {
this.parent = Array.from({ length: n }, (_, i) => i);
this.size = new Array<number>(n).fill(1);
this.count = n;
}
find(x: number): number {
if (this.parent[x] !== x) this.parent[x] = this.find(this.parent[x]);
return this.parent[x];
}
union(a: number, b: number): boolean {
let rootA = this.find(a);
let rootB = this.find(b);
if (rootA === rootB) return false;
if (this.size[rootA] < this.size[rootB]) [rootA, rootB] = [rootB, rootA];
this.parent[rootB] = rootA;
this.size[rootA] += this.size[rootB];
this.count--; // Two provinces just became one.
return true;
}
}
console.log(findCircleNum([[1, 1, 0], [1, 1, 0], [0, 0, 1]])); // 2
console.log(findCircleNum([[1, 0, 0], [0, 1, 0], [0, 0, 1]])); // 3
console.log(findCircleNum([[1, 0, 0, 1], [0, 1, 1, 0], [0, 1, 1, 1], [1, 0, 1, 1]])); // 1package main
import "fmt"
// findCircleNum returns how many groups of cities are linked, directly or
// through other cities.
func findCircleNum(isConnected [][]int) int {
n := len(isConnected)
ds := newDisjointSet(n) // Every city starts as its own province.
for i := range n {
// The matrix is symmetric, so the upper triangle has every road once.
for j := i + 1; j < n; j++ {
if isConnected[i][j] == 1 {
ds.union(i, j)
}
}
}
return ds.count
}
// disjointSet is the disjoint set from the template: path compression plus
// union by size.
type disjointSet struct {
parent []int
size []int
count int
}
func newDisjointSet(n int) *disjointSet {
ds := &disjointSet{parent: make([]int, n), size: make([]int, n), count: n}
for i := range n {
ds.parent[i] = i
ds.size[i] = 1
}
return ds
}
func (ds *disjointSet) find(x int) int {
if ds.parent[x] != x {
ds.parent[x] = ds.find(ds.parent[x])
}
return ds.parent[x]
}
func (ds *disjointSet) union(a, b int) bool {
rootA, rootB := ds.find(a), ds.find(b)
if rootA == rootB {
return false
}
if ds.size[rootA] < ds.size[rootB] {
rootA, rootB = rootB, rootA
}
ds.parent[rootB] = rootA
ds.size[rootA] += ds.size[rootB]
ds.count-- // Two provinces just became one.
return true
}
func main() {
fmt.Println(findCircleNum([][]int{{1, 1, 0}, {1, 1, 0}, {0, 0, 1}})) // 2
fmt.Println(findCircleNum([][]int{{1, 0, 0}, {0, 1, 0}, {0, 0, 1}})) // 3
fmt.Println(findCircleNum([][]int{{1, 0, 0, 1}, {0, 1, 1, 0}, {0, 1, 1, 1}, {1, 0, 1, 1}})) // 1
}using System;
public class Solution
{
// How many groups of cities are linked, directly or through other cities.
public static int FindCircleNum(int[][] isConnected)
{
int n = isConnected.Length;
var ds = new DisjointSet(n); // Every city starts as its own province.
for (int i = 0; i < n; i++)
{
// The matrix is symmetric, so the upper triangle has every road once.
for (int j = i + 1; j < n; j++)
{
if (isConnected[i][j] == 1) ds.Union(i, j);
}
}
return ds.Count;
}
// The disjoint set from the template: path compression plus union by size.
public class DisjointSet
{
readonly int[] parent;
readonly int[] size;
public int Count { get; private set; }
public DisjointSet(int n)
{
parent = new int[n];
size = new int[n];
for (int i = 0; i < n; i++)
{
parent[i] = i;
size[i] = 1;
}
Count = n;
}
public int Find(int x)
{
if (parent[x] != x) parent[x] = Find(parent[x]);
return parent[x];
}
public bool Union(int a, int b)
{
int rootA = Find(a), rootB = Find(b);
if (rootA == rootB) return false;
if (size[rootA] < size[rootB]) (rootA, rootB) = (rootB, rootA);
parent[rootB] = rootA;
size[rootA] += size[rootB];
Count--; // Two provinces just became one.
return true;
}
}
public static void Main()
{
Console.WriteLine(FindCircleNum(new[] { new[] { 1, 1, 0 }, new[] { 1, 1, 0 }, new[] { 0, 0, 1 } })); // 2
Console.WriteLine(FindCircleNum(new[] { new[] { 1, 0, 0 }, new[] { 0, 1, 0 }, new[] { 0, 0, 1 } })); // 3
Console.WriteLine(FindCircleNum(new[] { new[] { 1, 0, 0, 1 }, new[] { 0, 1, 1, 0 }, new[] { 0, 1, 1, 1 }, new[] { 1, 0, 1, 1 } })); // 1
}
}public class Solution {
// How many groups of cities are linked, directly or through other cities.
static int findCircleNum(int[][] isConnected) {
int n = isConnected.length;
DisjointSet ds = new DisjointSet(n); // Every city starts as its own province.
for (int i = 0; i < n; i++) {
// The matrix is symmetric, so the upper triangle has every road once.
for (int j = i + 1; j < n; j++) {
if (isConnected[i][j] == 1) ds.union(i, j);
}
}
return ds.count;
}
// The disjoint set from the template: path compression plus union by size.
static class DisjointSet {
private final int[] parent;
private final int[] size;
int count;
DisjointSet(int n) {
parent = new int[n];
size = new int[n];
for (int i = 0; i < n; i++) {
parent[i] = i;
size[i] = 1;
}
count = n;
}
int find(int x) {
if (parent[x] != x) parent[x] = find(parent[x]);
return parent[x];
}
boolean union(int a, int b) {
int rootA = find(a), rootB = find(b);
if (rootA == rootB) return false;
if (size[rootA] < size[rootB]) {
int swap = rootA;
rootA = rootB;
rootB = swap;
}
parent[rootB] = rootA;
size[rootA] += size[rootB];
count--; // Two provinces just became one.
return true;
}
}
public static void main(String[] args) {
System.out.println(findCircleNum(new int[][] {{1, 1, 0}, {1, 1, 0}, {0, 0, 1}})); // 2
System.out.println(findCircleNum(new int[][] {{1, 0, 0}, {0, 1, 0}, {0, 0, 1}})); // 3
System.out.println(findCircleNum(new int[][] {{1, 0, 0, 1}, {0, 1, 1, 0}, {0, 1, 1, 1}, {1, 0, 1, 1}})); // 1
}
}Problem
A graph started as a tree on the nodes 1 to n: connected, with no cycles. Then one extra edge was added between two different nodes. You get all n edges as a list edges. Return an edge whose removal leaves a tree again. If several would work, return the one that appears last in edges.
Example: [[1, 2], [1, 3], [2, 3]] returns [2, 3]. The three edges form a triangle, so removing any one of them works, and [2, 3] comes last.
Show SolutionHide Solution
Approach
Add the edges one at a time with union. While the edges so far contain no cycle, every union joins two different trees. The first edge whose ends already share a root is the one that closes a cycle, so return it.
Why is that the edge the problem wants? A tree plus one edge has exactly one cycle, and removing any edge on that cycle leaves a tree. The cycle doesn't exist until its last edge is added, so the edge that closes it is the cycle edge that appears latest in the input, which is exactly the tie-break the problem asks for. The third demo input shows this: the answer [1, 3] isn't the last edge overall, but it is the last edge of the cycle.
Nodes are numbered from 1, so the structure gets n + 1 slots and slot 0 goes unused.
Time O(n α(n))Space O(n)
def find_redundant_connection(edges: list[list[int]]) -> list[int]:
"""The edge to remove from a tree with one extra edge: the one that closes
the cycle, which is the cycle edge that appears last in `edges`."""
ds = DisjointSet(len(edges) + 1) # Nodes run 1..n, and n is len(edges).
for a, b in edges:
# Both ends already connected: this edge closes the cycle.
if not ds.union(a, b):
return [a, b]
return []
class DisjointSet:
"""The disjoint set from the template: path compression plus union by size."""
def __init__(self, n: int) -> None:
self.parent = list(range(n))
self.size = [1] * n
def find(self, x: int) -> int:
if self.parent[x] != x:
self.parent[x] = self.find(self.parent[x])
return self.parent[x]
def union(self, a: int, b: int) -> bool:
"""Returns False when a and b were already in the same set."""
root_a, root_b = self.find(a), self.find(b)
if root_a == root_b:
return False
if self.size[root_a] < self.size[root_b]:
root_a, root_b = root_b, root_a
self.parent[root_b] = root_a
self.size[root_a] += self.size[root_b]
return True
if __name__ == "__main__":
print(find_redundant_connection([[1, 2], [1, 3], [2, 3]])) # [2, 3]
print(find_redundant_connection([[1, 2], [2, 3], [3, 4], [1, 4], [1, 5]])) # [1, 4]
print(find_redundant_connection([[1, 2], [2, 3], [1, 3], [3, 4]])) # [1, 3]// The edge to remove from a tree with one extra edge: the one that closes
// the cycle, which is the cycle edge that appears last in `edges`.
export function findRedundantConnection(edges: number[][]): number[] {
const ds = new DisjointSet(edges.length + 1); // Nodes run 1..n, and n is edges.length.
for (const [a, b] of edges) {
// Both ends already connected: this edge closes the cycle.
if (!ds.union(a, b)) return [a, b];
}
return [];
}
// The disjoint set from the template: path compression plus union by size.
class DisjointSet {
private parent: number[];
private size: number[];
constructor(n: number) {
this.parent = Array.from({ length: n }, (_, i) => i);
this.size = new Array<number>(n).fill(1);
}
find(x: number): number {
if (this.parent[x] !== x) this.parent[x] = this.find(this.parent[x]);
return this.parent[x];
}
// Returns false when a and b were already in the same set.
union(a: number, b: number): boolean {
let rootA = this.find(a);
let rootB = this.find(b);
if (rootA === rootB) return false;
if (this.size[rootA] < this.size[rootB]) [rootA, rootB] = [rootB, rootA];
this.parent[rootB] = rootA;
this.size[rootA] += this.size[rootB];
return true;
}
}
console.log(JSON.stringify(findRedundantConnection([[1, 2], [1, 3], [2, 3]]))); // [2,3]
console.log(JSON.stringify(findRedundantConnection([[1, 2], [2, 3], [3, 4], [1, 4], [1, 5]]))); // [1,4]
console.log(JSON.stringify(findRedundantConnection([[1, 2], [2, 3], [1, 3], [3, 4]]))); // [1,3]package main
import "fmt"
// findRedundantConnection returns the edge to remove from a tree with one
// extra edge: the one that closes the cycle, which is the cycle edge that
// appears last in edges.
func findRedundantConnection(edges [][]int) []int {
ds := newDisjointSet(len(edges) + 1) // Nodes run 1..n, and n is len(edges).
for _, e := range edges {
// Both ends already connected: this edge closes the cycle.
if !ds.union(e[0], e[1]) {
return e
}
}
return nil
}
// disjointSet is the disjoint set from the template: path compression plus
// union by size.
type disjointSet struct {
parent []int
size []int
}
func newDisjointSet(n int) *disjointSet {
ds := &disjointSet{parent: make([]int, n), size: make([]int, n)}
for i := range n {
ds.parent[i] = i
ds.size[i] = 1
}
return ds
}
func (ds *disjointSet) find(x int) int {
if ds.parent[x] != x {
ds.parent[x] = ds.find(ds.parent[x])
}
return ds.parent[x]
}
// union returns false when a and b were already in the same set.
func (ds *disjointSet) union(a, b int) bool {
rootA, rootB := ds.find(a), ds.find(b)
if rootA == rootB {
return false
}
if ds.size[rootA] < ds.size[rootB] {
rootA, rootB = rootB, rootA
}
ds.parent[rootB] = rootA
ds.size[rootA] += ds.size[rootB]
return true
}
func main() {
fmt.Println(findRedundantConnection([][]int{{1, 2}, {1, 3}, {2, 3}})) // [2 3]
fmt.Println(findRedundantConnection([][]int{{1, 2}, {2, 3}, {3, 4}, {1, 4}, {1, 5}})) // [1 4]
fmt.Println(findRedundantConnection([][]int{{1, 2}, {2, 3}, {1, 3}, {3, 4}})) // [1 3]
}using System;
public class Solution
{
// The edge to remove from a tree with one extra edge: the one that closes
// the cycle, which is the cycle edge that appears last in edges.
public static int[] FindRedundantConnection(int[][] edges)
{
var ds = new DisjointSet(edges.Length + 1); // Nodes run 1..n, and n is edges.Length.
foreach (int[] edge in edges)
{
// Both ends already connected: this edge closes the cycle.
if (!ds.Union(edge[0], edge[1])) return edge;
}
return Array.Empty<int>();
}
// The disjoint set from the template: path compression plus union by size.
public class DisjointSet
{
readonly int[] parent;
readonly int[] size;
public DisjointSet(int n)
{
parent = new int[n];
size = new int[n];
for (int i = 0; i < n; i++)
{
parent[i] = i;
size[i] = 1;
}
}
public int Find(int x)
{
if (parent[x] != x) parent[x] = Find(parent[x]);
return parent[x];
}
// Returns false when a and b were already in the same set.
public bool Union(int a, int b)
{
int rootA = Find(a), rootB = Find(b);
if (rootA == rootB) return false;
if (size[rootA] < size[rootB]) (rootA, rootB) = (rootB, rootA);
parent[rootB] = rootA;
size[rootA] += size[rootB];
return true;
}
}
public static void Main()
{
Console.WriteLine(Format(FindRedundantConnection(new[] { new[] { 1, 2 }, new[] { 1, 3 }, new[] { 2, 3 } }))); // [2, 3]
Console.WriteLine(Format(FindRedundantConnection(new[] { new[] { 1, 2 }, new[] { 2, 3 }, new[] { 3, 4 }, new[] { 1, 4 }, new[] { 1, 5 } }))); // [1, 4]
Console.WriteLine(Format(FindRedundantConnection(new[] { new[] { 1, 2 }, new[] { 2, 3 }, new[] { 1, 3 }, new[] { 3, 4 } }))); // [1, 3]
}
static string Format(int[] values) => "[" + string.Join(", ", values) + "]";
}import java.util.Arrays;
public class Solution {
// The edge to remove from a tree with one extra edge: the one that closes
// the cycle, which is the cycle edge that appears last in edges.
static int[] findRedundantConnection(int[][] edges) {
DisjointSet ds = new DisjointSet(edges.length + 1); // Nodes run 1..n, and n is edges.length.
for (int[] edge : edges) {
// Both ends already connected: this edge closes the cycle.
if (!ds.union(edge[0], edge[1])) return edge;
}
return new int[0];
}
// The disjoint set from the template: path compression plus union by size.
static class DisjointSet {
private final int[] parent;
private final int[] size;
DisjointSet(int n) {
parent = new int[n];
size = new int[n];
for (int i = 0; i < n; i++) {
parent[i] = i;
size[i] = 1;
}
}
int find(int x) {
if (parent[x] != x) parent[x] = find(parent[x]);
return parent[x];
}
// Returns false when a and b were already in the same set.
boolean union(int a, int b) {
int rootA = find(a), rootB = find(b);
if (rootA == rootB) return false;
if (size[rootA] < size[rootB]) {
int swap = rootA;
rootA = rootB;
rootB = swap;
}
parent[rootB] = rootA;
size[rootA] += size[rootB];
return true;
}
}
public static void main(String[] args) {
System.out.println(Arrays.toString(findRedundantConnection(new int[][] {{1, 2}, {1, 3}, {2, 3}}))); // [2, 3]
System.out.println(Arrays.toString(findRedundantConnection(new int[][] {{1, 2}, {2, 3}, {3, 4}, {1, 4}, {1, 5}}))); // [1, 4]
System.out.println(Arrays.toString(findRedundantConnection(new int[][] {{1, 2}, {2, 3}, {1, 3}, {3, 4}}))); // [1, 3]
}
}Common Pitfalls
- Checking
parent[a] == parent[b]to test whether two items are connected. Two elements can have different parents and still share a root. Comparefind(a) == find(b). - Linking the elements instead of their roots.
parent[a] = bmoves onlyaand the elements below it, and leaves the rest of its old group behind. Always linkfind(a)andfind(b). - Decrementing the group count on every
unioncall, including the ones where the two items were already connected. - Allocating
nslots when nodes are numbered from 1 to n. Usen + 1and ignore slot 0. - Expecting union-find to handle deleted edges. It only merges, so process the operations in reverse or use a graph traversal.
Practice
More problems to try on your own.