Disjoint Set?
분리 집합(Disjoint Set, 유니언 파인드라고도 불림)은 중복되지 않는 여러 개의 집합을 관리하는 자료구조입니다. 이 자료구조는 두 가지 주요 연산을 제공합니다:
- Find: 특정 요소가 속한 집합(또는 대표자)을 찾습니다.
- Union: 두 개의 집합을 하나로 합칩니다.
이 자료구조는 동적 연결성 문제를 해결할 때 매우 유용하며, 두 요소가 같은 집합에 속해 있는지 또는 다른 집합에 속해 있는지 효율적으로 판별할 수 있습니다.
분리 집합의 구성 요소
- 부모 배열: 각 요소는 자신의 부모를 가리키며, 자신을 가리킬 경우 그 요소는 집합의 대표자(또는 루트)입니다.
- 랭크에 따른 Union: 두 집합을 합칠 때, 더 작은 트리를 더 큰 트리 아래에 병합하여 구조의 균형을 유지합니다.
- 경로 압축:
find
연산을 수행할 때, 각 노드에서 루트로 가는 경로를 평탄화하여 이후의find
연산을 더 빠르게 처리합니다.
사용 사례
- 그래프의 연결된 컴포넌트 찾기: 두 정점이 같은 연결 컴포넌트에 속해 있는지 판별.
- 그래프에서 사이클 감지: 무향 그래프에서 사이클이 있는지 확인.
- 최소 신장 트리(크루스칼 알고리즘): 효율적으로 최소 신장 트리(MST)를 찾는 데 사용.
- 동적 연결 문제: 동적인 네트워크에서 연결된 컴포넌트를 추적하는 문제.
시간 복잡도
분리 집합의 연산(Find와 Union)은 거의 상수 시간 복잡도인 O(α(n))을 가집니다. 여기서 α(n)은 매우 천천히 증가하는 역 아커만 함수입니다.
JavaScript로 구현한 분리 집합
다음은 경로 압축과 랭크에 따른 유니언이 포함된 JavaScript 구현 예시입니다:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
class DisjointSet {
constructor(size) {
this.parent = new Array(size);
this.rank = new Array(size).fill(0);
// 각 요소가 자기 자신을 부모로 가지도록 초기화
for (let i = 0; i < size; i++) {
this.parent[i] = i;
}
}
// 경로 압축을 사용해 'x'가 속한 집합의 루트를 찾음
find(x) {
if (this.parent[x] !== x) {
this.parent[x] = this.find(this.parent[x]); // 경로 압축
}
return this.parent[x];
}
// 랭크에 따른 유니언
union(x, y) {
let rootX = this.find(x);
let rootY = this.find(y);
if (rootX !== rootY) {
// 랭크에 따른 유니언: 작은 트리를 큰 트리 아래에 병합
if (this.rank[rootX] > this.rank[rootY]) {
this.parent[rootY] = rootX;
} else if (this.rank[rootX] < this.rank[rootY]) {
this.parent[rootX] = rootY;
} else {
this.parent[rootY] = rootX;
this.rank[rootX]++;
}
}
}
// 두 요소가 같은 집합에 속해 있는지 확인
connected(x, y) {
return this.find(x) === this.find(y);
}
}
// 사용 예시:
let ds = new DisjointSet(5); // 0부터 4까지 5개의 분리 집합 생성
ds.union(0, 1);
ds.union(1, 2);
console.log(ds.connected(0, 2)); // 출력: true
console.log(ds.connected(0, 3)); // 출력: false
ds.union(3, 4);
console.log(ds.connected(3, 4)); // 출력: true
설명:
find
메서드는 주어진 요소의 루트를 찾아 경로 압축을 적용해 트리를 평탄화합니다.union
메서드는 두 집합을 병합하며, 트리의 균형을 유지하기 위해 랭크를 사용합니다.connected
는 두 요소가 같은 집합에 속해 있는지를 확인합니다.
Rust로 구현한 분리 집합
다음은 경로 압축과 랭크에 따른 유니언이 포함된 Rust 구현 예시입니다:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
struct DisjointSet {
parent: Vec<usize>,
rank: Vec<usize>,
}
impl DisjointSet {
// 'size' 개의 요소를 가진 새로운 분리 집합 생성
fn new(size: usize) -> DisjointSet {
let mut parent = Vec::with_capacity(size);
for i in 0..size {
parent.push(i);
}
DisjointSet {
parent,
rank: vec![0; size],
}
}
// 경로 압축을 사용해 'x'가 속한 집합의 루트를 찾음
fn find(&mut self, x: usize) -> usize {
if self.parent[x] != x {
self.parent[x] = self.find(self.parent[x]); // 경로 압축
}
self.parent[x]
}
// 랭크에 따른 유니언
fn union(&mut self, x: usize, y: usize) {
let root_x = self.find(x);
let root_y = self.find(y);
if root_x != root_y {
// 랭크에 따른 유니언: 작은 트리를 큰 트리 아래에 병합
if self.rank[root_x] > self.rank[root_y] {
self.parent[root_y] = root_x;
} else if self.rank[root_x] < self.rank[root_y] {
self.parent[root_x] = root_y;
} else {
self.parent[root_y] = root_x;
self.rank[root_x] += 1;
}
}
}
// 두 요소가 같은 집합에 속해 있는지 확인
fn connected(&mut self, x: usize, y: usize) -> bool {
self.find(x) == self.find(y)
}
}
fn main() {
let mut ds = DisjointSet::new(5); // 0부터 4까지 5개의 분리 집합 생성
ds.union(0, 1);
ds.union(1, 2);
println!("{}", ds.connected(0, 2)); // 출력: true
println!("{}", ds.connected(0, 3)); // 출력: false
ds.union(3, 4);
println!("{}", ds.connected(3, 4)); // 출력: true
}
설명:
find
는 경로 압축을 사용해 트리 구조를 평탄화하여 더 효율적인 탐색을 제공합니다.union
은 랭크를 사용하여 더 작은 트리를 더 큰 트리 아래에 병합합니다.connected
함수는 두 요소가 같은 집합에 속해 있는지를 확인합니다.
핵심 요점:
- 분리 집합은 동적인 환경에서 집합을 관리하고 병합하는 효율적인 자료구조입니다.
- 크루스칼의 최소 신장 트리나 사이클 감지와 같은 그래프 관련 알고리즘에서 자주 사용됩니다.
- 경로 압축과 랭크에 따른 유니언을 통해
find
와union
연산은 거의 상수 시간 복잡도를 달성합니다.
Disjoint Set?
The Disjoint Set (also known as Union-Find or Merge-Find Set) is a data structure that manages a collection of non-overlapping sets. It provides two main operations:
- Find: Determines the set (or representative) that a particular element belongs to.
- Union: Merges two sets into one.
This structure is very useful for dealing with dynamic connectivity problems, where we need to efficiently determine whether two elements are in the same set or belong to different sets.
Components of Disjoint Set
- Parent Array: Each element points to its parent, and if it points to itself, it’s the representative (or root) of the set.
- Union by Rank: When merging two sets, the smaller tree (rank-wise) is merged under the root of the larger tree to keep the structure balanced.
- Path Compression: During the
find
operation, the path from each node to the root is flattened, which helps to speed up futurefind
operations by reducing the tree’s height.
Use Cases
- Connected Components in a Graph: Determining if two vertices are in the same connected component.
- Cycle Detection in a Graph: Checking for cycles in an undirected graph.
- Minimum Spanning Tree (Kruskal’s Algorithm): To find the Minimum Spanning Tree (MST) of a graph efficiently.
- Dynamic Connectivity Problems: Tracking connected components in dynamic networks.
Time Complexity
The Disjoint Set operations (find and union) have an almost constant time complexity of O(α(n)), where α(n) is the inverse Ackermann function, which grows extremely slowly.
Disjoint Set in JavaScript
Here’s a JavaScript implementation of the Disjoint Set with path compression and union by rank:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
class DisjointSet {
constructor(size) {
this.parent = new Array(size);
this.rank = new Array(size).fill(0);
// Initialize each element to be its own parent
for (let i = 0; i < size; i++) {
this.parent[i] = i;
}
}
// Find the root of the set that element 'x' belongs to with path compression
find(x) {
if (this.parent[x] !== x) {
this.parent[x] = this.find(this.parent[x]); // Path compression
}
return this.parent[x];
}
// Union by rank
union(x, y) {
let rootX = this.find(x);
let rootY = this.find(y);
if (rootX !== rootY) {
// Union by rank: attach smaller tree under larger tree
if (this.rank[rootX] > this.rank[rootY]) {
this.parent[rootY] = rootX;
} else if (this.rank[rootX] < this.rank[rootY]) {
this.parent[rootX] = rootY;
} else {
this.parent[rootY] = rootX;
this.rank[rootX]++;
}
}
}
// Check if two elements are in the same set
connected(x, y) {
return this.find(x) === this.find(y);
}
}
// Usage Example:
let ds = new DisjointSet(5); // Create 5 disjoint sets (0 to 4)
ds.union(0, 1);
ds.union(1, 2);
console.log(ds.connected(0, 2)); // Output: true
console.log(ds.connected(0, 3)); // Output: false
ds.union(3, 4);
console.log(ds.connected(3, 4)); // Output: true
Explanation:
- The
find
method determines the root of a given element, applying path compression to flatten the tree for efficiency. - The
union
method merges two sets, using rank to ensure that the smaller tree is always attached to the larger tree. connected
checks whether two elements are in the same set.
Disjoint Set in Rust
Here’s a Rust implementation of the Disjoint Set (Union-Find) with path compression and union by rank:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
struct DisjointSet {
parent: Vec<usize>,
rank: Vec<usize>,
}
impl DisjointSet {
// Initialize a new Disjoint Set with 'size' elements
fn new(size: usize) -> DisjointSet {
let mut parent = Vec::with_capacity(size);
for i in 0..size {
parent.push(i);
}
DisjointSet {
parent,
rank: vec![0; size],
}
}
// Find the root of the set that element 'x' belongs to with path compression
fn find(&mut self, x: usize) -> usize {
if self.parent[x] != x {
self.parent[x] = self.find(self.parent[x]); // Path compression
}
self.parent[x]
}
// Union by rank
fn union(&mut self, x: usize, y: usize) {
let root_x = self.find(x);
let root_y = self.find(y);
if root_x != root_y {
// Union by rank: attach smaller tree under larger tree
if self.rank[root_x] > self.rank[root_y] {
self.parent[root_y] = root_x;
} else if self.rank[root_x] < self.rank[root_y] {
self.parent[root_x] = root_y;
} else {
self.parent[root_y] = root_x;
self.rank[root_x] += 1;
}
}
}
// Check if two elements are in the same set
fn connected(&mut self, x: usize, y: usize) -> bool {
self.find(x) == self.find(y)
}
}
fn main() {
let mut ds = DisjointSet::new(5); // Create 5 disjoint sets (0 to 4)
ds.union(0, 1);
ds.union(1, 2);
println!("{}", ds.connected(0, 2)); // Output: true
println!("{}", ds.connected(0, 3)); // Output: false
ds.union(3, 4);
println!("{}", ds.connected(3, 4)); // Output: true
}
Explanation:
find
uses path compression to flatten the tree structure for more efficient future lookups.union
merges two sets by attaching the smaller set under the larger set based on rank.- The
connected
function checks if two elements are in the same set by comparing their root representatives.
Key Points:
- Disjoint Set is an efficient data structure for managing and merging sets in dynamic environments.
- It is commonly used in graph-related algorithms like Kruskal’s Minimum Spanning Tree and Cycle Detection.
- By using path compression and union by rank, Disjoint Set achieves nearly constant time complexity for find and union operations.