도입
개요
- Union-Find는 상호 배타적으로 이루어진 집합을 표현하기 위해 만들어진 자료구조로, Disjoint Set(서로소 집합)이라고도 한다.
- 서로소 관계에 있는 복수의 집합에 대하여 집합들을 병합하는 Union 연산과 원소가 어떤 집합에 속해있는지 판단하는 Find 연산을 지원하여 Union Find라는 이름이 붙었다.
- 서로소 : 공통 원소가 없는, 교집합이 공집합인 집합들 사이의 관계.
- 원소 간의 연결 여부를 판단하는 데 유용하다.
- 기본적으로 트리 구조이지만 그 구조가 큰 의미를 가지지는 않는 특이한 그래프 알고리즘이다.
Find 연산
- Find 연산은 입력된 노드의 부모를 재귀적으로 거슬러 올라가 최상위 노드의 값을 반환한다. 이 최상위 노드는 각 집합과 일대일 대응되어 노드의 소속을 알 수 있다.
- Big O 표기법으로 O(n)이 걸린다고 한다.
예시
#include<bits/stdc++.h>
using namespace std;
int find(int i)
{
if (parent[i] == i) {
return i;
}
else {
return find(parent[i]);
}
}
최적화
- Find 연산에서 방문하는 각 노드마다 결과값을 반환하기 전에 리스트에 저장하여 경로를 압축할 수 있다.
- 이 경우 평균 O(log n)으로 단축된다.
- 예시
#include <bits/stdc++.h>
using namespace std;
int find(int i)
{
if (Parent[i] == i) {
return i;
}
else {
int result = find(Parent[i]);
Parent[i] = result;
return result;
}
}
Union 연산
- 두 개 이상의 인자를 받아 Find 연산으로 그 집합을 찾고, 이를 하나의 최상위 노드 아래에 하나의 트리로 병합한다.
- 서로소 집합만을 다루므로 합집합 연산과 같다.
- Find 연산만이 시간에 영향을 미쳐, 시간복잡도는 Find 연산과 동일하다.
예시
#include <bits/stdc++.h>
using namespace std;
void union(int i, int j) {
int irep = this.Find(i),
int jrep = this.Find(j);
this.Parent[irep] = jrep;
}
최적화
- Union 연산은 최악의 경우에 트리를 편중시킬 수 있다.
- 이를 해결하기 위해 별개의 리스트를 만들어 어떠한 기준에 따라 큰 트리(집합)에 작은 트리(집합)을 합치도록 할 수 있다.
- Union by Rank : 트리의 깊이에 따라 병합
- Union by Size : 집합의 크기에 따라 병합
적용 예시
#include <bits/stdc++.h>
using namespace std;
class DisjSet {
int *rank, *parent, n;
public:
DisjSet(int n)
{
rank = new int[n];
parent = new int[n];
this->n = n;
makeSet();
}
void makeSet()
{
for (int i = 0; i < n; i++) {
parent[i] = i;
}
}
int find(int x)
{
if (parent[x] != x) {
parent[x] = find(parent[x]);
}
return parent[x];
}
void Union(int x, int y)
{
int xset = find(x);
int yset = find(y);
if (xset == yset)
return;
if (rank[xset] < rank[yset]) {
parent[xset] = yset;
}
else if (rank[xset] > rank[yset]) {
parent[yset] = xset;
}
else {
parent[yset] = xset;
rank[xset] = rank[xset] + 1;
}
}
};
int main()
{
DisjSet obj(5);
obj.Union(0, 2);
obj.Union(4, 2);
obj.Union(3, 1);
if (obj.find(4) == obj.find(0))
cout << "Yes\n";
else
cout << "No\n";
if (obj.find(1) == obj.find(0))
cout << "Yes\n";
else
cout << "No\n";
return 0;
}
#include <iostream>
#include <vector>
using namespace std;
int N, M;
int knowTruth;
int parent[53];
int temp;
vector<int> party[53];
int partyNum;
int lie;
int findParent(int x)
{
if (parent[x] != x)
return parent[x] = findParent(parent[x]);
return x;
}
void merge(int a, int b)
{
int x = findParent(a);
int y = findParent(b);
if (x != y)
{
if (x < y)
parent[y] = x;
else
parent[x] = y;
}
}
int main()
{
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
cin >> N >> M;
cin >> knowTruth;
for (int i = 0; i <= N; ++i)
parent[i] = i;
while (knowTruth--)
{
cin >> temp;
parent[temp] = 0;
}
for (int i = 0; i < M; ++i)
{
cin >> partyNum;
cin >> temp;
party[i].push_back(temp);
for (int j = 1; j < partyNum; ++j)
{
cin >> temp;
party[i].push_back(temp);
merge(party[i][0], party[i][j]);
}
}
lie = M;
for (int i = 0; i < M; ++i)
{
for (int j : party[i])
{
if (findParent(parent[j]) == 0)
{
--lie;
break;
}
}
}
cout << lie;
return 0;
}
참조