Sack - Disjoint Set Union on Tree
Prerequisite
- Small to Large
- HLD
Intro
이 글은 코드포스의 이 블로그 를 기반으로 작성되었다.
어떤 문제를 풀다 Tree DP로 쉽게 될 것 같은데? 라고 생각했던 값이 쉽게 구해지지 않는것에서 의아함을 느꼈고 포스팅을 작성하게 되었다.
Sack
Sack이란건 생소한데, 그냥 저 블로그 포스터가 붙인 이름인거같다.
실제 DSU를 사용하는건 아닌것같아 보이지만 Small To Large 테크닉을 이용한 트리에서의 테크닉의 한 종류라고 생각하면 된다.
다음과 같은 쿼리를 생각해보자.
트리에서 각 정점들이 임의의 정수를 가질 때, $u$의 서브트리에서 $x$ 를 값으로 가지는 정점의 개수는 몇개인가?
만약 문제에서 $x$가 고정이 되어 있다면 Tree DP로 쉽게 구할 수 있겠지만, 만약 임의의 $x$에 대해 $u$ 에서 모두 쿼리를 빠르게 날리고 싶다면 쉽지 않음을 알 수 있다.
Euler tour technic을 이용한다면 서브트리의 정점들이 연속된 구간들로 표현이 되기 때문에 $in_u \sim out_u$ 의 범위안에 있는 정점들로 다른 자료구조를 섞어서 쓰면 가능할지도(?) 이 부분은 잘 모르겠다.
어쨌든 Sack이라는 테크닉은 이러한 종류의 쿼리를 처리해주는 테크닉이라고 할 수 있다.
주의해야 할 점
첫 번째는 위에서 언급한 정수가 몇개가 있느니 하는 쿼리에 국한되지 않는다는 점이다. 일반화해서 말하자면 “$u$ 의 서브트리에 자유도 높은 쿼리를 날릴 후 있다” 정도이다.
두 번째는 알고리즘이 종료된 후에도 막 임의의 정점에 대해 원하는 쿼리를 때려버릴 수는 없다는 것이다.
아래 구현에서 설명할거지만, DFS를 진행하며 어떤 결과 자료구조를 계속해서 관리하는 방식이기 때문에 $u$ 정점을 순회하고 있을 때만 $u$ 정점에서 쿼리를 날릴 수 있다.
필요한 값이 있다면 그 때 쿼리를 하거나 그 때 저장해두는 방식으로 추후에 이용함은 당연히 가능하다.
시간복잡도
$O(n \log n)$ 이다. 트리에서 Small to large를 기반으로 동작되기 때문이다.
자료구조를 사용하면 추가 상수가 붙음에 유의
구현
원글을 참고하면 $O(n \log n)$ 이라는 세 가지 구현 방법이 나오는데, 모두 읽어봤는데 특별히 구현 난이도나 차이는 크지 않은것같아 기호에 맞게 선택해서 익히면 좋을 것 같다.
모든 dfs 함수는 다음과 같은 형태를 띈다.
void dfs(int cur, int p, bool keep) { ... }
이는 현재 정점과 부모 정점, 그리고 현재 정점에서 처리된 값을 롤백하지 않고 유지할지를 결정하는 플래그이다.
주된 아이디어는 다음과 같다. 하나의 정점을 처리할 때 다음과 같은 일들이 순차적으로 진행된다.
- 서브트리의 크기들을 전처리해둔다. 이를 $S$ 라고 하자.
- 자식 중, 가장 무거운 자식 정점을 찾고 그걸 $B$라고 하자.
- HLD에서 그 무거운 간선의 개념이 맞다. 가장 서브트리의 크기가 큰 자식을 의미한다.
- 가벼운 자식 정점은
dfs(to, cur, false)
처럼 호출을 한다.- 가벼운 자식 정점으로 DFS를 돌려주고 거기서 빠져나올 때, 처리된 값을 롤백하라는 의미이다.
- 무거운 자식 정점은
dfs(to, cur, true)
를 호출한다.- 무거운 자식 정점은 처리된 값을 롤백하지 않는다.
- 현재 정점의 값을 자료구조에에 업데이트한다.
- 가벼운 자식 정점들은 다시 그 서브트리의 정점들을 순회하며 값을 하나씩 직접 자료구조에 업데이트한다.
- 만약
keep=false
라면 현재 서브트리의 값을 롤백한다.
Sack 구현
Sack은 원글에서도 소개되었지만 Euler tour Technic을 이용해 각 정점마다 들어가는 시간(dfsn), 나오는 시간을 얻고 어떤 순서 $t$ 에 어떤 정점이 들어갔는지도 저장해둔다.
이 과정을 이용해 각 정점마다 서브트리에 있는 정점들의 배열을 따로 관리할 필요 없이 쉽게 정점의 서브트리의 정점들을 순회할 수 있다.
다음과 같은 문제를 보자.
풀이 코드는 다음과 같다.
in
: ETT에서 들어가는 순서(inclusive)out
: ETT에서 나오는 순서(inclusive)in_rev
:in
의 역함수size
: 서브트리 크기c
: 입력으로 들어오는 정점들의 색 $(1 \sim n)$color
: 어떤 색이 현재 존재하는 개수sum
: 현재 $k$개인 색들의 합mx_sum
현재 가장 많이 나온 수의 개수
int mx_sum = 0;
map<int, int> color, sum;
auto add = [&](int x) {
sum[color[x]] -= x;
color[x]++;
sum[color[x]] += x;
mx_sum = max(mx_sum, color[x]);
};
auto remove = [&](int x) {
sum[color[x]] -= x;
color[x]--;
sum[color[x]] += x;
if (mx_sum == color[x] + 1 && sum[color[x] + 1] == 0)
mx_sum = color[x];
};
function<void(int, int, bool)> sack = [&](int cur, int p, bool keep) -> void {
int B = -1;
for (int to: edges[cur]) {
if (to == p) continue;
if (B == -1 || size[B] < size[to]) B = to;
}
for (int to: edges[cur])
if (to != p && to != B)
sack(to, cur, false);
if (B != -1)
sack(B, cur, true);
add(c[cur]);
for (int to: edges[cur])
if (to != p && to != B)
for (int i = in[to]; i <= out[to]; i++)
add(c[in_rev[i]]);
ans[cur] = sum[mx_sum];
if (!keep)
for (int i = in[cur]; i <= out[cur]; i++)
remove(c[in_rev[i]]);
};
sack(0, -1, 1);
연습 문제
더 많은 문제는 원문 링크에서 풀어볼 수 있다.
원문에 없는 문제들을 좀 골라보자.
이 문제도 기본 문제이다.
MEX를 쉽게 구하는 값은 $0 \sim N-1$ 의 값을 모두 set
으로 관리하고 자료구조에 어떤 $x$가 들어오면 그 set
에서 그걸 제거한 다음 가장 작은 원소를 구하면 된다.
만약 모든 수가 있다면 set
이 빌 수도 있으므로 구현의 편의성을 위해 $N$ 도 넣고 시작하자.
이 문제는 굳이 이렇게 안구하고 정직하게 Smaller to large를 써도 되지만, 이렇게도 풀 수 있다.
Comments