각 노드 간 연결성을 파악하는 dynamic connectivity 문제를 해결하기 위한 알고리즘이다. 복잡하게 얽힌 네트워크 속에서 특정 노드들 간의 연결 여부를 확인해야할 때, 효과적으로 문제를 해결할 수 있다. 노드의 연결 여부를 확인하는 것에만 중점을 두며, 거리 또는 최단 거리 등 연결 여부 외 다른 정보에는 관심을 갖지 않는다.
구현 연산은 다음과 같다.
여기서 "연결"은 다음의 등가 관계를 가정한다.
위 그림을 union, connected 연산을 통해 나타내면 다음과 같다.
union(3, 4)
union(3, 8)
union(5, 6)
union(4, 9)
union(1, 2)
union(0, 5)
union(2, 7)
union(1, 6)
union(0, 1)
connected(8, 1) => false
connected(8, 9) => true
connected(0, 7) => true
{0, 1, 2, 5, 6, 7} {3, 4, 8, 9} => connected components
array, tree 등을 사용해서 구현할 수 있는 disjoint set 자료구조를 사용한다. array를 통해 구현하는 경우 index는 노드를 의미하고, element는 노드와 연결된 노드를 의미한다.
disjoint set은 서로 중복되는 공통 원소가 없는 상호 배타적인 부분 집합들로 나눠진 원소들을 표현할 수 있는 자료구조이다.
알고리즘을 구현하기 위한 API 설계는 다음과 같다.
union find를 구현한 가장 간단한 알고리즘이다. 노드가 연결될 때마다 모든 노드를 순회하면서 연결된 노드를 변경한다.
init(10)
index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|---|
element | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
union(1, 0)
index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|---|
element | 0 | 0 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
union(4, 3)
index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|---|
element | 0 | 0 | 2 | 3 | 3 | 5 | 6 | 7 | 8 | 9 |
union(2, 4)
index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|---|
element | 0 | 0 | 3 | 3 | 3 | 5 | 6 | 7 | 8 | 9 |
union(7, 3)
index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|---|
element | 0 | 0 | 3 | 3 | 3 | 5 | 6 | 3 | 8 | 9 |
union(7, 0)
index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|---|
element | 0 | 0 | 0 | 0 | 0 | 5 | 6 | 0 | 8 | 9 |
public class QuickFind {
private int[] nodes;
public QuickFind(int n) {
init(n);
}
public void init(int n) {
nodes = new int[n];
for (int i = 0; i < n; i++) {
nodes[i] = i;
}
}
public boolean connected(int a, int b) {
return nodes[a] == nodes[b];
}
public void union(int a, int b) {
int aNode = nodes[a];
int bNode = nodes[b];
for (int i = 0; i < nodes.length; i++) {
if (nodes[i] == aNode) {
nodes[i] = bNode;
}
}
}
}
quick find 알고리즘의 union 연산을 개선하기 위한 알고리즘이다. 노드가 속한 집합을 트리 구조로 형성하고, 노드가 속한 트리의 루트 노드와 연결된 노드만 변경한다.
init(10)
index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|---|
element | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
union(7, 2)
index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|---|
element | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 2 | 8 | 9 |
union(3, 8)
index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|---|
element | 0 | 1 | 2 | 8 | 4 | 5 | 6 | 2 | 8 | 9 |
union(9, 4)
index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|---|
element | 0 | 1 | 2 | 8 | 4 | 5 | 6 | 2 | 8 | 4 |
union(3, 7)
index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|---|
element | 0 | 1 | 2 | 8 | 4 | 5 | 6 | 2 | 2 | 4 |
union(9, 3)
index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|---|
element | 0 | 1 | 2 | 8 | 2 | 5 | 6 | 2 | 2 | 4 |
union(0, 5) -> union(6, 5) -> union(1, 2) -> union(0, 1)
index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|---|
element | 5 | 2 | 2 | 8 | 2 | 2 | 5 | 2 | 2 | 4 |
public class QuickUnion {
private int[] nodes;
public QuickUnion(int n) {
init(n);
}
public void init(int n) {
nodes = new int[n];
for (int i = 0; i < n; i++) {
nodes[i] = i;
}
}
private int root(int i) {
while (i != nodes[i]) {
i = nodes[i];
}
return i;
}
public boolean connected(int a, int b) {
return root(a) == root(b);
}
public void union(int a, int b) {
int i = root(a);
int j = root(b);
nodes[i] = j;
}
}
index | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|---|
element | 0 | 0 | 1 | 2 | 3 | 4 | 5 |
quick union 알고리즘의 union 연산을 개선하기 위한 알고리즘이다. 크기가 작은 트리를 크기가 큰 트리로 병합해서 트리의 전체 높이가 증가하는 것을 최소화하여 균형이 무너지는 것을 방지할 수 있다.
init(10)
index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|---|
element | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
union(2, 7)
index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|---|
element | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 2 | 8 | 9 |
union(3, 8)
index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|---|
element | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 2 | 3 | 9 |
union(4, 9)
index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|---|
element | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 2 | 3 | 4 |
union(6, 7)
index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|---|
element | 0 | 1 | 2 | 3 | 4 | 5 | 2 | 2 | 3 | 4 |
union(7, 3)
index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|---|
element | 0 | 1 | 2 | 2 | 4 | 5 | 2 | 2 | 3 | 4 |
union(9, 8)
index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|---|
element | 0 | 1 | 2 | 2 | 2 | 5 | 2 | 2 | 3 | 4 |
union(0, 1) -> union(5, 0) -> union(6, 0)
index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|---|
element | 0 | 0 | 2 | 2 | 2 | 0 | 2 | 2 | 3 | 4 |
union(9, 5) - case: weighted quick union
index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|---|
element | 2 | 0 | 2 | 2 | 2 | 0 | 2 | 2 | 3 | 4 |
union(9, 5) - case: quick union
index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|---|
element | 0 | 0 | 0 | 2 | 2 | 0 | 2 | 2 | 3 | 4 |
public class WeightedQuickUnion {
private int[] nodes;
private int[] sizes;
public WeightedQuickUnion(int n) {
init(n);
}
public void init(int n) {
this.nodes = new int[n];
for (int i = 0; i < n; i++) {
nodes[i] = i;
sizes[i] = 1;
}
}
private int root(int i) {
while (i != nodes[i]) {
i = nodes[i];
}
return i;
}
public boolean connected(int a, int b) {
return root(a) == root(b);
}
public void union(int a, int b) {
int i = root(a);
int j = root(b);
if (i == j) {
return;
}
if (sizes[i] < sizes[j]) {
nodes[i] = j;
sizes[j] += sizes[i];
} else {
nodes[j] = i;
sizes[i] += sizes[j];
}
}
}
트리의 높이 | 0 | 1 | 2 | 3 | 4 | 5 | ... | log |
---|---|---|---|---|---|---|---|---|
트리의 크기 | 1 | 2 | 4 | 8 | 16 | 32 | ... | N |
quick union 알고리즘을 더욱 개선하기 위한 알고리즘이다. 연산시 루트 노드까지의 경로를 압축시켜 지속적으로 트리의 높이를 낮은 형태로 재구성한다.
init(10)
index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|---|
element | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
union(2, 7)
index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|---|
element | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 2 | 8 | 9 |
union(3, 8)
index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|---|
element | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 2 | 3 | 9 |
union(4, 9)
index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|---|
element | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 2 | 3 | 4 |
union(6, 7)
index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|---|
element | 0 | 1 | 2 | 3 | 4 | 5 | 2 | 2 | 3 | 4 |
union(7, 3)
index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|---|
element | 0 | 1 | 2 | 2 | 4 | 5 | 2 | 2 | 3 | 4 |
union(9, 8)
index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|---|
element | 0 | 1 | 2 | 2 | 2 | 5 | 2 | 2 | 2 | 4 |
union(0, 1) -> union(5, 0) -> union(6, 0)
index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|---|
element | 0 | 0 | 2 | 2 | 2 | 0 | 2 | 2 | 2 | 4 |
union(9, 5)
index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|---|
element | 2 | 0 | 2 | 2 | 2 | 0 | 2 | 2 | 2 | 2 |
connected(1, 5)
index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|---|
element | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 |
public class WeightedQuickUnionWithPathComperssion {
private int[] nodes;
private int[] sizes;
public WeightedQuickUnionWithPathComperssion(int n) {
init(n);
}
public void init(int n) {
this.nodes = new int[n];
for (int i = 0; i < n; i++) {
nodes[i] = i;
sizes[i] = 1;
}
}
private int root(int i) {
while (i != nodes[i]) {
nodes[i] = nodes[nodes[i]];
i = nodes[i];
}
return i;
}
public boolean connected(int a, int b) {
return root(a) == root(b);
}
public void union(int a, int b) {
int i = root(a);
int j = root(b);
if (i == j) {
return;
}
if (sizes[i] < sizes[j]) {
nodes[i] = j;
sizes[j] += sizes[i];
} else {
nodes[j] = i;
sizes[i] += sizes[j];
}
}
}
알고리즘 | 시간 복잡도(N개의 노드 init + M번의 union) |
---|---|
Quick Find | N M |
Quick Union | N M |
Weighted Quick Union | N + M log |
Quick Union With Path Compression | N + M log |
Weighted Quick Union With Path Compression | N + M logN |
N개의 노드 init, M번의 union 연산을 수행했을 때, 시간 복잡도는 O(N + M logN)이다. logN의 증가 속도가 매우 느리기 때문에 선형 시간 복잡도는 아니지만 그것에 가까운 시간 복잡도라고 할 수 있다.
logN(iterated logarithm, log star)는 N의 로그 함수를 수행한 결과 값을 반복적으로 수행해서 결과 값이 1이하가 나올때까지 수행된 횟수이다.
ex) N = 65536, logN = 4 (log -> log -> log -> log)