문제 출처
1. 문제설명
- $1, 2, …, N$으로 번호가 매겨진 $N$개의 정점이 있다.
- 다음과 같은 쿼리를 수행한 결과를 출력하라
1 u v
: 정점 u
, v
를 잇는 간선 추가
2
: 정점의 개수가 가장 많은 트리의 번호 출력
- 문제 분류
- Data Structure, Set and Map using Tree, Disjoint set
2. 알고리즘 설계
- Union-Find 문제이다.
- 트리가 제거되는 경우는 2가지 이다.
- 사이클이 존재하는 경우
2
연산을 수행한 경우
2
연산의 경우 정렬된 {노드 번호, 연결된 자식의 수}
자료에서 가장 앞의 원소를 꺼내면 되겠다.
- 사이클 처리
Parent[u]
, Parent[v]
의 자료를 지운다.
- 두 개의 값이 같다면 사이클임을 표시한다.
- 이후, 둘 중 하나라도 사이클이 있다면 자료를 지우고 끝이다.
- 둘 다 사이클이 없다면 연결된 자식 수가 많은 노드를 부모 노드로 취하고, 부모 노드의 연결된 자식 수를 갱신한다.
3. 전체 코드
#include <bits/stdc++.h>
using namespace std;
const int SZ = 100'002;
int N, Q, P[SZ], Cy[SZ], Size[SZ];
struct T{
int r, s;
bool operator<(const T& t) const { return s ^ t.s ? s > t.s : r < t.r; }
};
set<T> S;
int Find(int x) { return P[x] = x == P[x] ? x : Find(P[x]); }
void Merge(int x, int y) {
S.erase({x, Size[x]});
S.erase({y, Size[y]});
Cy[x] += (x == y);
if(x ^ y) {
if(Cy[x] + Cy[y]) Cy[x] = Cy[y] = 1;
else {
if (x > y) S.insert({ P[x] = y, Size[y] += Size[x] });
else S.insert({ P[y] = x, Size[x] += Size[y] });
}
}
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(NULL);
cin >> N >> Q;
for(int i=1; i<=N; i++) S.insert({P[i] = i, Size[i] = 1});
for(int o, u, v; Q--;) {
cin >> o;
if(o == 1) {
cin >> u >> v;
Merge(Find(u), Find(v));
}
else {
cout << (*S.begin()).r << '\n';
S.erase(S.begin());
}
}
}
4. 코멘트
- 이 문제 진짜 너무 어려웠다.
- 로직 관련
- 먼저 자료를 지운 후, 이후에 다시 추가한다.
- 처음에는 vector에 사이클이 존재하는 노드를 저장해두었다. 근데, 이렇게 하니까 메모리 초과가 났다…
- set을 이용한 아이디어는 내가 떠올린 게 아니라, 한참동안 방법을 못 찾겠어서 검색을 통해 확인할 수 있었다.
- 개인적으로 union-find를 활용한 끝판왕 문제였다.
x ^ y
- 이 연산자는 피연산자 2개의 값이 서로 다르면 True이다.
- 2중 저장
- 아래의 코드를 보면 저장과 동시에 저장하고 있다.
S.insert({ P[x] = y, Size[y] += Size[x] });
- 평소 나였다면, 아래와 같이 작성했을텐데, 상기 코드가 더 깔끔하고 좋은 거 같다.
P[x] = y, Size[y] += Size[x], S.insert({P[x], Size[y]});