서로소 집합
공통원소가 없는 두 집합
서로소 집합은 대표적으로 두 가지 연산을 수행합니다
1. Find(X): X가 속한 집합의 대표 찾기
2. Union(X, Y) : X가 속한 집합과 Y가 속한 집합을 합침
따라서 서로소 집합을 이용하는 알고리즘을 유니온파인드라고 말합니다
유니온파인드 연산
일반적인 배열을 사용해서도 유니온파인드 연산을 수행할 수 있습니다
이 경우 Union연산 수행을 위해 배열의 모든 값을 돌면서 대표를 변경해야하므로 시간복잡도는 O(N)입니다
유니온파인드 알고리즘을 사용하는 이유는 이 시간복잡도를 O(a(N)), 약 O(1)의 시간복잡도로 줄이기 위해서입니다
최초에는 각 원소는 각자 자신이 대표가 됩니다
원소 | 0 | 1 | 2 | 3 | 4 |
대표 | 0 | 1 | 2 | 3 | 4 |
- Find
임의로 1번 원소를 0번 집합에 포함시키고 2번 원소를 1번 집합에 포함시키겠습니다
원소 | 0 | 1 | 2 | 3 | 4 |
대표 | 0 | 0 | 1 | 3 | 4 |
2번 원소의 대표는 어떻게 찾을까요? 원소 == 대표가 될때까지 대표를 따라 이동하면 됩니다
private int[] parent;
public int find(int x) {
if (parent[x] != x) {
return find(parent[x]);
}
return x;
}
그러나 이 코드는 최악의 경우, 모든 원소가 일렬로 연결된 경우(예: 0 - 1 - 2 - 3 - 4)에는 O(N)의 시간 복잡도를 가집니다.
시간복잡도를 줄이기 위해서 find로 대표 원소를 찾으면 대표값을 바로 위 집합의 원소가 아니라 대표 원소로 변경해줍시다
public int find(int x) {
if (parent[x] != x) {
parent[x] = find(parent[x]);
}
return parent[x];
}
이제 find 함수를 실행하면 배열은 다음과 같이 변경되고 find의 시간복잡도는 거의 O(1)이 됐습니다
원소 | 0 | 1 | 2 | 3 | 4 |
대표 | 0 | 0 | 0 | 3 | 4 |
- Union
이번에는 임의로 합치지 않고 실제로 합치는 Union 함수를 구현해보겠습니다
Union 함수는 각 대표값을 찾고 한쪽의 대표를 반대쪽 대표로 변경해주면 됩니다
public void union(int x, int y) {
int rootX = find(x);
int rootY = find(y);
if (rootX != rootY) {
parent[rootX] = rootY;
}
}
union(3, 4)로 합치면 배열은 다음과 같이 변경됩니다
원소 | 0 | 1 | 2 | 3 | 4 |
대표 | 0 | 0 | 0 | 4 | 4 |
이 메서드에도 한 가지 추가적으로 고려할 점이 있습니다
배열이 다음과 같다고 가정합시다
원소 | 0 | 1 | 2 | 3 | 4 |
대표 | 0 | 0 | 1 | 4 | 4 |
Union을 아무렇게나 합쳐도 괜찮을까요?
현재 0 집합의 최대 깊이는 2, 4집합의 최대 깊이는 1입니다
2의 깊이는 2이고 4의 깊이는 0입니다
union(4 , 2)를 수행할 경우
원소 | 0 | 1 | 2 | 3 | 4 |
대표 | 0 | 0 | 1 | 4 | 0 |
최대 깊이는 여전히 0 - 1 - 2로 변하지 않습니다
union(2, 4)를 수행할 경우
원소 | 0 | 1 | 2 | 3 | 4 |
대표 | 4 | 0 | 1 | 4 | 4 |
최대 깊이는 4 - 0 - 1 - 2로 깊이가 1증가했습니다
즉, 트리의 깊이가 큰 쪽을 작은쪽으로 합치는 경우 트리의 깊이가 계속 증가하여 시간복잡도는 O(N)이 되고
깊이가 작은 쪽을 큰 쪽으로 합치는 경우 시간복잡도는 거의 O(1)로 줄일 수 있으므로 항상 깊이가 작은 쪽을 큰 쪽으로 합치도록 코드를 수정하겠습니다
public void union(int x, int y) {
int rootX = find(x);
int rootY = find(y);
if (rootX == rootY) return;
if (rank[rootX] < rank[rootY]) {
parent[rootX] = rootY;
} else if (rank[rootX] > rank[rootY]) {
parent[rootY] = rootX;
} else {
parent[rootY] = rootX;
rank[rootX]++;
}
}
이 코드는 깊이가 작은 쪽을 큰 쪽으로 합칩니다. 이 경우, 트리의 깊이는 변하지 않습니다.
만일 두 깊이가 동일한 경우 깊이는 1 증가합니다
최종적인 유니온 파인드 코드는 다음과 같습니다
public class UnionFind {
private int[] parent;
private int[] rank;
public UnionFind(int size) {
parent = new int[size];
rank = new int[size];
for (int i = 0; i < size; i++) {
parent[i] = i;
rank[i] = 0;
}
}
public int find(int x) {
if (parent[x] != x) {
parent[x] = find(parent[x]);
}
return parent[x];
}
public void union(int x, int y) {
int rootX = find(x);
int rootY = find(y);
if (rootX == rootY) return;
if (rank[rootX] < rank[rootY]) {
parent[rootX] = rootY;
} else if (rank[rootX] > rank[rootY]) {
parent[rootY] = rootX;
} else {
parent[rootY] = rootX;
rank[rootX]++;
}
}
}
유니온파인드 대표 연산
- 원소가 속한 집합의 대표 찾기 : find(x)
- 두 원소가 같은 집합에 속하는지 확인 : find(x) == find(y)
- 두 집합을 합치기(커뮤니티에 포함, 합병) : union(x)
- 집합 갯수 세기 : 대표값을 모은 Set의 크기 계산
- 그래프에 사이클을 포함하는지 확인 : find(node1) == find(node2)이면 두 노드 간 사이클 존재
- 최소 스패닝 트리 : 간선의 가중치가 작은 순서로 처리하면서 find(node1) != find(node2) 이면 union(node1, node2)
'코딩테스트' 카테고리의 다른 글
[백준] 9663 - N-queen (0) | 2025.01.25 |
---|---|
Java TreeSet, TreeMap, 그리고 Red-Black Tree (3) | 2024.09.22 |
이진 검색(이분 탐색) 완벽 가이드: 개념부터 문제 풀이까지 with. 프로그래머스 징검다리 (1) | 2024.09.08 |
비트마스킹 가이드 : 기본 개념과 실전 예제 (0) | 2024.08.25 |
효율적인 코딩 테스트를 위한 Java 자료구조 선택 가이드 (0) | 2024.08.11 |