유니온 파인드 알고리즘
개요
유니온 파인드란?
- 유니온 파인드는 그래프(트리) 알고리즘의 한 종류이다.
- 어떤 두 개의 노드가 같은 그래프에 속해 있는지 판별하는 알고리즘이다.
- 서로소 집합, 상호 베타적 집합이라고도 한다.
- 연산 종류
- Union 연산
- 두 개의 트리를 합쳐, 하나의 그래프를 만드는 연산
- Find 연산
- 어떤 트리의 루트 노드를 찾는 연산
- Union 연산
- 주로 배열을 활용하여, 트리를 표현한다.
지금부터 각 연산에 대해 알아보자.
Union 연산
- 아래 그림과 같이, 각 노드가 아무와도 연결되어 있지 않은 상태라고 생각해보자.
- 배열의 index는 노드의 번호를 나타내고, value는 해당 노드의 부모노드를 나타낸다.
- 이때 우리는 노드를
{1, 2}
,{6, 7, 8}
과 같이, 연결하고자 한다.
- 배열의 value 값이 해당 노드의 부모 노드를 나타내므로, 각 index의 value 값을 변경하면 된다.
- 이는 아래 그림과 같다.
- index 2번의 value 값이
2 -> 1
로 변경되었다.
- 이제 나머지에 대해, 마저 수행해보자.
하지만 이러한 방식은 개선의 여지가 남아있다.
Find 연산을 통해, 어떤 비효율성이 숨어있는지 확인하고 개선해보자.
Find 연산
- 위에서 만든 그래프에서 노드 8번이 어떤 트리에 속해있는지 확인해보자.
- 이와 같은 절차로, 8번 노드가 어떤 트리에 속해있는지 확인할 수 있다.
- 하지만, 아래와 같이 편중된 상황에서 Find 연산을 수행하게 되면 어떻게 될까?
- Find 연산의 시간복잡도는 최악의 경우,
O(n)
이다.단말 노드부터 탐색을 진행하는 경우
- 이러한 문제를 해결하기 위해선, Union 연산을 개선하면 된다.
- 계속해서 알아보자.
개선된 Union 연산
- 위와 같은 문제를 개선하기 위해, Union 연산을 아래와 같이 수행하도록 하자.
- 노드의 value를 수정할 때, ‘부모노드의 번호’가 아닌 ‘루트노드의 번호’로 수정
- 예를 들어
{3, 4, 5}
로 묶는 경우, 아래와 같이 수행된다.
- 위와 같이 수행한다면, Find 연산은 다음과 같이 수행된다.
- 이때,
{1, 2}
로 묶고,{1, 2}
와{3, 4, 5}
를 묶는다면 아래와 같이 될 것이다.
코드
Find 연산 코드
/**
* Find 메서드
* @param nodeNumber 찾을 노드번호
* @return 찾은 루트노드 번호
*/
public int find(int nodeNumber) {
if (nodeNumber == array[nodeNumber]) { //노드 번호와 값이 같다면
return nodeNumber; //해당 노드가 루트노드인 트리에 속해있다.
}
return find(array[nodeNumber]);
}
Union 연산 코드
/**
* Union 메서드 (Union은 예약어인 경우가 많아, 보통 merge로 명명한다.)
* @param x 합칠 노드 1
* @param y 합칠 노드 2
*/
public void merge(int x, int y) {
int rootNodeOfX = find(x); //x 노드의 루트노드 번호
int rootNodeOfY = find(y); //y 노드의 루트노드 번호
//만약 두 노드의 루트노드가 같다면, 이미 연결되어있는 것이므로 종료
if (x == y) {
return;
}
//작은 번호가 루트 노드가 되도록
if (rootNodeOfX < rootNodeOfY) {
//루트노드 번호로 갱신
array[rootNodeOfY] = rootNodeOfX;
} else {
//루트노드 번호로 갱신
array[rootNodeOfX] = rootNodeOfY;
}
}
전체 코드
public class UnionFind {
int[] array = new int[] {0, 1, 2, 3, 4, 5, 6, 7};
/**
* Find 메서드
* @param nodeNumber 찾을 노드번호
* @return 찾은 루트노드 번호
*/
public int find(int nodeNumber) {
if (nodeNumber == array[nodeNumber]) { //노드 번호와 값이 같다면
return nodeNumber; //해당 노드가 루트노드인 트리에 속해있다.
}
array[nodeNumber] = find(array[nodeNumber]); //경로 압축을 위해, 상단 루트노드로 저장한다.
return array[nodeNumber];
}
/**
* Union 메서드 (Union은 예약어인 경우가 많아, 보통 merge로 명명한다.)
* @param x 합칠 노드 1
* @param y 합칠 노드 2
*/
public void merge(int x, int y) {
int rootNodeOfX = find(x); //x 노드의 루트노드 번호
int rootNodeOfY = find(y); //y 노드의 루트노드 번호
//만약 두 노드의 루트노드가 같다면, 이미 연결되어있는 것이므로 종료
if (x == y) {
return;
}
//작은 번호가 루트 노드가 되도록
if (rootNodeOfX < rootNodeOfY) {
//루트노드 번호로 갱신
array[rootNodeOfY] = rootNodeOfX;
} else {
//루트노드 번호로 갱신
array[rootNodeOfX] = rootNodeOfY;
}
}
/**
* 두 노드가 연결되어있는지 판별하는 연산
* @param x
* @param y
* @return
*/
public boolean isUnion(int x, int y) {
int rootNodeOfX = find(x);
int rootNodeOfY = find(y);
if (rootNodeOfX == rootNodeOfY) {
return true;
}
return false;
}
}
실행 코드
public class Main {
public static void main(String[] args) {
UnionFind u = new UnionFind();
u.merge(1, 2);
u.merge(4, 5);
u.merge(5, 6);
u.merge(1, 5);
System.out.println(u.find(4)); //출력: 1
}
}