Union-Find 자료구조

Union-Find란 이름에서도 알 수 있듯, 서로소 집합 관리에 특화된 데이터 구조입니다. 이 구조는 특정 원소가 속해 있는 집합을 관리하며, 두 집합의 병합(Union)과 특정 원소가 속한 집합의 대표를 찾는 Find 연산을 지원합니다.

이 구조는림수 (Forest)로 표현할 수 있습니다. 각각의 트리가 하나의 집합을 대표하며, 트리의 모든 노드는 그 집합에 속한 원소를 나타냅니다.

예를 들어 아래 그림과 같이:

이 그림은 두 개의 집합을 나타내며, 각각의 집합은 {1,2,3,4,5}와 {6,7,8}로 이루어져 있습니다.

두 집합을 병합하는 방법은 간단합니다. 한 집합의 루트 노드를 다른 집합의 루트 노드로 연결시켜주면 됩니다:

병합 연산의 핵심은 두 집합의 대표를 같게 만드는 것입니다. 반면, 조회 연산은 해당 원소의 최상위 루트 노드를 찾는 것이며, 이는 집합에 속하는지 여부를 판별하는 데 사용됩니다. 예를 들어, 원소 x와 y가 같은 집합에 속하면 find(x)와 find(y)의 결과가 동일합니다.

기본적인 find와 union 연산의 시간 복잡도는 O(N)이며, 이는 최악의 경우 트리가 체인 구조로 될 때 발생합니다. 이를 개선하기 위해 경로 압축(Path Compression) 기법을 사용합니다. 경로 압축은 찾기 연산 중에서 현재 노드의 대표를 직접 연결시켜주는 기법으로, 시간 복잡도를 O(log N)로 개선합니다.

경로 압축 외에도 사이즈 병합(Size-based Union) 기법을 사용하면 추가적인 최적화를 얻을 수 있습니다. 사이즈 병합은 항상 더 큰 사이즈의 집합으로 더 작은 사이즈의 집합을 병합시켜주는 방법으로, 평균적인 조회 시간을 O(α(N))로 줄일 수 있습니다. α(N)은 거의 상수에 가까운 느리게 성장하는 함수입니다.

다음은 기본적인 Union-Find 구현 예제입니다:

#include<bits/stdc++.h>
using namespace std;
const int MAXN=200010;
int parent[MAXN];
int find(int x) {
    if (x != parent[x]) {
        parent[x] = find(parent[x]); // 경로 압축
    }
    return parent[x];
}
void union_set(int x, int y) {
    int fx = find(x);
    int fy = find(y);
    if (fx != fy) {
        parent[fy] = fx;
    }
}
int main() {
    // 입력 및 초기화
    int n, m;
    cin >> n >> m;
    for (int i = 0; i < n; ++i) {
        parent[i] = i;
    }
    for (int i = 0; i < m; ++i) {
        int z, x, y;
        cin >> z >> x >> y;
        if (z == 1) {
            if (find(x) != find(y)) {
                union_set(x, y);
            }
        } else if (z == 2) {
            if (find(x) == find(y)) {
                cout << "Y" << endl;
            } else {
                cout << "N" << endl;
            }
        }
    }
    return 0;
}

상기 코드는 경로 압축만을 사용한 기본적인 구현입니다. 사이즈 병합을 추가한 버전은 다음과 같습니다:

#include<bits/stdc++.h>
using namespace std;
const int MAXN=200010;
int parent[MAXN], size[MAXN];
int find(int x) {
    if (x != parent[x]) {
        parent[x] = find(parent[x]); // 경로 압축
    }
    return parent[x];
}
void union_set(int x, int y) {
    int fx = find(x);
    int fy = find(y);
    if (fx == fy) return;
    if (size[fx] < size[fy]) {
        parent[fx] = fy;
        size[fy] += size[fx];
    } else {
        parent[fy] = fx;
        size[fx] += size[fy];
    }
}
int main() {
    // 입력 및 초기화
    int n, m;
    cin >> n >> m;
    for (int i = 0; i < n; ++i) {
        parent[i] = i;
        size[i] = 1;
    }
    for (int i = 0; i < m; ++i) {
        int z, x, y;
        cin >> z >> x >> y;
        if (z == 1) {
            if (find(x) != find(y)) {
                union_set(x, y);
            }
        } else if (z == 2) {
            if (find(x) == find(y)) {
                cout << "Y" << endl;
            } else {
                cout << "N" << endl;
            }
        }
    }
    return 0;
}

이 버전은 사이즈 병합을 통해 더 나은 성능을 제공합니다. 경로 압축과 사이즈 병합을 동시에 사용하면 거의 최적의 성능을 얻을 수 있습니다.

태그: Union-Find data-structure Path-Compression Size-Based-Union

7월 15일 23:56에 게시됨