분리 집합(Disjoint Set, Union-Find)은 코테에서 중요한 자료구조로 나타남
Disjoint Set(분리 집합, 서로소 집합)이란 서로 공통된 원소를 가지고 있지 않은 2개 이상의 집합을 말함
분리집합은 여러 노드가 서로 중복되지 않는 집합으로 나누어져 있을 때, 각 집합을 대표하는 노드 하나를 선택해 그 집합을 대표하도록 구성함
코드
const int SIZE = 100001;
int parent[SIZE];
// 1번부터 SIZE - 1번의 노드를 초기화함
void init() {
for (int i = 0; i < SIZE; i++) {
parent[i] = i;
}
}

Find 연산은 어떤 원소가 주어졌을 때 이 원소가 속한 집합의 대표 원소(루트 노드)를 반환하는 연산임

위 사진처럼 7이라는 노드에 find 연산을 하면 1이라는 값을 반환해야됨
이걸 단순하게 구현하려면 이렇게 구현하면 됨
int find(int x) {
if (x == parent[x])
return x;
return find(parent[x]);
}
하지만 위 사진을 예시로 들어서 find(7)이라는 연산을 실행했을 때는 다음과 같음
재귀 과정
find(7) → 7의 부모 6, find(6) 호출find(6) → 6의 부모 4, find(4) 호출find(4) → 4의 부모 2, find(2) 호출find(2) → 2의 부모 1, find(1) 호출find(1) → 1 == parent[1], 루트 노드이므로 1 반환총 5번의 함수 호출
만약, n번째 노드가 n level이라면 O(n)의 시간복잡도를 가지게 됨
find연산을 n번 호출하면 O()이라는 시간복잡도를 갖게 됨 이는 매우 비효율적임
이런 문제를 해결하기 위해 경로 단축을 활용함
int find(int x) {
if (x == parent[x])
return x;
return parent[x] = find(parent[x]);
}
위 코드와 차이점은 return parent[x]를 할 때 find(parent[x])의 반환값으로 초기화한 뒤 반환한다는 점이다.
개선된 find 연산을 통해 위와 동일한 find(7)을 실행해보자
동작 과정
find(7) → return parent[7] = find(parent[7]) ⇒ find(6) 호출, 이때 parent[7]은 find(6)의 반환값으로 초기화됨return parent[6] = find(parent[6]) ⇒ find(4) 호출, 이때 parent[6]은 find(4)의 반환값으로 초기화됨return parent[4] = find(parent[4]) ⇒ find(2) 호출, 이때 parent[4]은 find(2)의 반환값으로 초기화됨return parent[2] = find(parent[2]) ⇒ find(1) 호출, 이때 parent[2]은 find(1)의 반환값으로 초기화됨결과 ⇒ parent[7], parent[6], parent[4], parent[2]의 값이 모두 1로 초기화됨
이는 추후에 7의 직계 조상 (부모, 부모의 부모, …) 노드들에 find 연산을 했을 때 O(1)의 시간 복잡도를 갖게 해준다.

두 개의 집합을 하나의 집합으로 합치는 연산이다.
즉, 한 트리의 루트를 다른 트리의 루트에 연결하여 두 트리를 하나로 결합한다.
Union 과정은 다음과 같다.

union 연산을 하면 그림과 같이 되는 것이다.
// x, y 두 요소의 루트 노드를 찾은 뒤, y의 루트 노드를 x의 루트 노드 아래로 연결시킨다.
void merge(int x, int y) {
int rootX = find(x);
int rootY = find(y);
if (rootX == rootY) {
return;
}
parent[rootY] = rootX;
}
하지만 이 방식의 최악의 경우 다음과 같은 선형 트리가 생길 수 있음

이런 경우, find 연산이 O(n)이라는 시간복잡도를 갖게 됨
두 트리를 합칠 때 트리의 높이(깊이, rank)를 기준으로 더 작은 트리를 더 큰 트리 아래에 연결한다.
직관적인 이유
높이가 더 낮은 트리를 더 큰 트리 아래에 연결하면, 합쳐진 트리의 최대 높이는 증가하지 않거나 1만큼만 증가함
이를 통해 균형을 유지하면서 트리의 깊이가 비정상적으로 커지는 것을 방지할 수 있음
이를 Union by Rank 기법이라고 한다.
증명
- 기본 가정
- 처음 각 원소가 자기 자신을 부모로 가지므로 모든 집합의 트리의 높이는 0
- Rank는 트리의 높이를 의미함
- Rank 증가 조건
- 두 트리를 합칠 때 Rank가 같은 경우에만 높이 (Rank)가 증가함
- 두 트리의 높이가 모두 h라면, 두 트리를 합친 결과의 높이는 h+1이 됨
- Rank의 최대값
- Rank가 증가하는 횟수는 제한적임
- 노드가 합쳐질 때마다 한 쪽 트리의 Rank는 그대로 유지되고, 다른 쪽 트리의 Rank가 증가함
- n개의 노드가 있을 때, 트리의 Rank는 최대 log N을 넘지 않음


Base case
1개의 노드만 있을 때 트리의 높이 h = 0
Hypothesis
Union by Rank를 사용해서 두 트리를 합친 트리의 노드 수가 k개 일 때, 트리의 높이는 를 넘지 않는다고 가정
귀납
두 개의 높이가 이하인 트리를 합치면
이때, 트리의 최대 높이는 를 넘지 않음
따라서 Union by Rank를 사용해서 두 트리를 합친 트리의 노드 수가 N개일 때 트리의 높이는 최대 이 됨
개선 코드
int parent[SIZE];
int ranking[SIZE];
void init() {
for (int i = 0; i < SIZE; i++) {
parent[i] = i;
ranking[i] =0;
}
}
//find는 동일
void merge(int x, int y) {
int rootX = find(x);
int rootY = find(y);
if (rootX == rootY) {
return;
}
if (ranking[rootX] > ranking[rootY]) {
parent[rootY] = rootX;
}
else if(ranking[rootX] < ranking[rootY]) {
parent[rootX] = rootY;
}
else {
ranking[rootX]++;
parent[rootY] = rootX;
}
}
최적화를 하지 않은 union-find의 시간복잡도는 O(N) 이다. (N은 노드의 수)
경로 압축과 union by rank를 적용한 union-find의 시간복잡되는 O(α(N))이다.
O(α(N))는 아커만 역함수로 매우 느리게 증가하는 함수로 N이 이어도 아커만 역함수는 4 이하이다.
따라서, 실제 문제에서는 O(1)과 같다.
부모와의 관계를 나타내는 가중치를 저장하며, 상대적 관계나 거리, 값을 유지하는 데 사용할 수 있다.
💡사용예시
예시 코드
const int SIZE = 100001;
int parent[SIZE];
int weight[SIZE];
void init() {
for (int i = 0; i < SIZE; i++) {
parent[i] = i;
weight[i] = 0;
}
}
int find(int x) {
if (x != parent[x]) {
int root = find(parent[x]); // 루트 노드 찾기
weight[x] += weight[parent[x]]; // 가중치 누적
parent[x] = root; // 경로 압축
}
return parent[x];
}
void merge(int x, int y, int w) {
int rootX = find(x);
int rootY = find(y);
if (x == y) {
return;
}
parent[rootY] = rootX;
weight[rootY] = weight[x] - weight[y] + w;
// weight[x] : 노드x와 루트 노드와의 상대적 가중치
// weight[y] : 노드y와 루트 노드와의 상대적 가중치
// w : 노드x와 노드y 사이의 직접적인 가중치
// weight[x] - weight[y] : 노드x와 루트 노드와의 상대적 가중치
// 이를 통해 x와 y 간의 기존 상대적 가중치 관계를 유지함
// + w : x와 y 사이에 새로운 가중치 w를 추가함
// 결과 weight[rootY]는 rootY가 rootX 아래로 병합될 때, rootY와 rootX 간의 가중치
// 차이를 정확히 반영함
}
weight[rootY] = weight[x] - weight[y] + w의 관한 GPT의 부가 설명
예시
이때, unionWeighted(1, 2, 4)와 unionWeighted(2, 3, 3)를 호출한다고 가정합니다.
과정
unionWeighted(1, 2, 4) 실행 시:weight[root2] = weight[1] - weight[2] + 4unionWeighted(2, 3, 3) 실행 시:weight[root3] = weight[2] - weight[3] + 3weight[2] = 4이므로:weight[3]=4−0+3=7 weight[3]=4−0+3=7결과
정리
수식 weight[rootY] = weight[x] - weight[y] + w;는:
x와 노드 y가 속한 집합을 병합할 때,w를 조정하여이 수식을 통해 가중치 기반의 Union-Find에서 각 노드 간의 상대적인 관계나 거리 값을 정확하게 관리할 수 있습니다. 🚀