티스토리 뷰
(이 글은 독자가 Splay tree, link/cut tree를 이해하고 있다고 가정하고 설명한다.)
Top tree는 동적 트리 (간선의 추가, 제거가 일어나는 트리)를 관리하는 자료구조이다. Top tree는 path 쿼리와 subtree 쿼리를 모두 지원하며, 트리의 diameter, center, median과 같은 일반적인 자료구조에서 관리하기 힘든 값들을 관리할 수 있다. 이 글은 Self-Adjusting Top Tree 논문에 기반하여 Top Tree의 구현에 대해 설명한다.
(이 논문에 대해서 구사과님이 쓰신 글도 있다.)
Top tree
직선형 구조를 관리하는 자료구조인 세그먼트 트리는 전체 배열을 여러 구간으로 분리하고, 이 구간들을 합치는 과정을 트리로 관리한다. Top tree는 이 방식을 트리로 확장한다.
배열의 '구간'과 같은 개념을 트리에도 적용하기 위해, 비슷한 개념인 클러스터를 생각한다. 트리에서 다음 조건을 만족하는 서브트리를 클러스터라고 정의한다.
- 서브트리 외부의 정점과 연결된 정점의 개수가 2개 이하이다.
주의할 점은 클러스터는 정점이 아닌 간선의 집합으로 본다. 이제 클러스터를 통해 세그먼트 트리를 트리로 확장해 보자.
세그먼트 트리는 다음과 같은 두 종류의 노드로 이루어진 트리이다.
- 배열의 한 값만을 나타내는 리프 노드
- 두 자식 구간을 합친 구간을 나타내는 리프가 아닌 노드
클러스터에 기반한 Top tree에서도 비슷하게 정의할 수 있다.
- 하나의 간선만을 나타내는 리프 노드 (base cluster)
- 두 자식 클러스터를 합친 클러스터를 나타내는 리프가 아닌 노드

이는 트리에서 분할 정복을 하는 것으로 볼 수 있으므로, 매우 일반화된 형태이다. 하지만 이를 효율적으로 관리하는 것은 쉽지 않다. 효율적이고 간단한 구현을 위해, Top tree를 조금 다르게 정의해보자.
먼저 트리의 각 정점에 대해 연결되어있는 간선들이 어떤 circular order를 가지고 있다고 생각한다. (circular order를 부여하는 이유는 나중에 설명할 Top tree의 연산들에 순서가 필요하기 때문이다.)
그리고 이 트리에서 두 연산 Compress와 Rake를 정의한다.
- Compress(v) : 차수가
인 정점 에 인접한 두 정점 에 대해, 두 간선 와 를 합쳐서 한 간선 로 대체한다. - Rake(v) : 차수가
인 정점 에 인접한 정점 에 대해, 간선 와 circular order 상에서 인접한 다른 간선 를 합쳐서 한 간선 로 대체한다. (이를 가 에 Rake되었다고 말할 것이다.)

이 연산들을 계속 반복하면 마지막에는 하나의 간선만이 남을 것이다. 이 과정에서 나타나는 간선들은 클러스터를 나타내고, Compress/Rake 연산은 두 클러스터를 합치는 연산을 나타낸다고 생각할 수 있다.
이제 Top Tree를 다음 세 가지 종류의 노드를 가진 이진 트리로 정의한다.
- 하나의 간선만을 나타내는 리프 노드 (base cluster)
- 두 자식 노드를 Compress한 간선을 나타내는 Compress 노드
- 두 자식 노드를 Rake한 간선을 나타내는 Rake 노드
Top tree의 구성
전체 트리를 Top tree로 표현하기 위해서는 Compress/Rake를 통해 전체 트리를 표현하는 클러스터를 만들어야 한다.
이를 위해 리프 정점 하나를 선택하여 루트로 만들고, 전체 트리를 리프에서 시작해서 루트 방향으로 가는 경로들로 나눈다. 각 경로는 루트 정점에서 끝나거나 다른 경로에서 끝나는데, 루트 정점에서 끝나는 경로를 루트 경로라고 하자.

위 그림과 같이 루트 경로는
만약 바깥 클러스터가 없다면, 리프가 아닌
실제 자료구조에서는, 편의를 위해 따로 Rake 노드를 만들지 않고 Rake한 두 클러스터를 직접 Compress 노드에 연결한다. 즉, Compress 노드는 4개의 자식을 가지게 된다. (이 때 Compress 노드의 원래의 자식을 proper child, Rake 연산으로 온 자식을 foster child라 부른다.)

왼쪽의 foster child가 왼쪽의 proper child에 Rake되고, 오른쪽의 foster child가 오른쪽의 proper child에 Rake된다. 그리고 compress된 정점을 기준으로 [왼쪽 foster child] - [왼쪽 proper child] - [오른쪽 foster child] - [오른쪽 proper child] 순으로 circular order를 가지게 된다. 물론, foster child가 없을 수도 있다.

루트 경로가 아닌 경로에 대해서도 동일한 방법으로 경로를 한 클러스터로 압축할 수 있다. 한 정점의 바깥 클러스터에서 여러 개의 경로가 들어올 경우에는 이 경로들을 전부 Rake해서 하나로 묶어주면 된다.

여기까지의 Top tree의 구성 방법을 정리하면 다음과 같다.
- 루트 경로를 선택하고 루트 경로에 인접한 경로들을 재귀적으로 계산하여 하나의 클러스터로 묶는다.
- Rake tree를 통해 한 정점에 들어오는 여러 개의 경로들을 하나의 클러스터로 묶는다.
- Compress tree를 통해 루트 경로를 하나의 클러스터로 묶는다.
구성 과정을 보았을 때, Compress tree는 하나의 경로를 압축하고
한 가지 주의할 점은 Rake tree 안에서도 circular order를 유지해야 한다. 또한 Compress tree가 관리하는 경로의 순서도 유지할 수 있다. 위 그림들의 Compress tree를 보면 오른쪽으로 갈수록 루트 노드에 가까워진다. 하지만 항상 Compress tree의 순서를 유지하면서 구현하는 것은 쉽지 않다. 그러므로 구현할 때 Compress tree가 경로의 순서를 유지하지 않을 수 있다고 생각한다. (왼쪽 서브트리와 오른쪽 서브트리가 자유롭게 바뀔 수 있다.) 두 서브트리를 바꾸더라도 circular order는 유지된다.
1. 앞으로 '노드/클러스터의 끝점' 이라는 표현을 사용할 것인데, 이는 그 클러스터를 루트로 하는 Compress tree가 압축한 경로의 끝점을 의미한다.
Handle
Top tree는 간선을 관리하는 자료이기 때문에, 정점 표현을 지원하지 않는다. 하지만 Top tree의 연산들(Expose, Link, Cut)은 정점에 대한 연산이기 때문에 정점 표현이 가능해야 한다.
정점 표현을 지원하기 위해서 정점
Splay
Top tree의 효율성을 보장하기 위해 Top tree의 높이를 최대한 낮게 유지해야 한다. 이 글에서는 Splay를 통해 트리의 높이를

기본적으로 Splay tree에서 하는 일반적인 Splay와 큰 차이는 없다. Compress 노드를 Rotate할 때 foster child를 어떻게 바꿔야 할 지 헷갈릴 수 있는데, 사실 바꿀 필요가 없다. 원래 트리의 구조가 바뀌지 않기 때문에 각 노드의 foster child는 바뀌지 않는다.
Splay 연산을 올바르게 적용하기 위해서는 Top tree가 '올바른 순서'를 가지고 있어야 한다. Compress tree가 경로의 순서를 유지하지 않으므로 Splay를 하기 전에 이 순서를 조정해줄(rectify) 필요가 있다.
오른쪽으로 갈수록 루트 정점에 가까워지도록 Compress tree의 순서를 조정하는 방법을 생각하자. (왼쪽이여도 상관없다.) 노드
가 Compress tree의 루트가 아닐 경우 : 의 부모를 , 의 오른쪽 proper child을 이라 하자. 의 양 끝 정점이 , 일 경우 순서가 올바르다. 가 Compress tree의 루트일 경우 : 의 오른쪽 proper child을 이라 하자. 의 가 아닌 끝 정점이 리프가 아닐 경우 순서가 올바르다. 가 전체 Top tree의 루트일 경우 : 두 자식을 바꿔도, 바꾸지 않아도 된다. 두 자식을 바꾼다면 루트 정점이 바뀌게 된다.
위 방법을 사용하기 위해서는 Compress 노드가 관리하는 경로의 양 끝 정점을 알아야 한다. 이는 두 proper child의 정보를 통해 쉽게 알 수 있다.
Splice
link/cut tree에서는 access 연산을 통해 어떤 정점이 포함된 경로를 루트 경로로 만든다. Top tree에서는 splice 연산을 통해 이를 수행한다.

splice 연산은 트리의 경로 파티션을 바꾼다.

실제 Top Tree에서
이 그림은 splice의 가능한 case중 하나일 뿐이다. 모든 case에서


Soft Expose
Expose는 Top tree에서 특정 경로
처음으로
- Local Splay :
부터 루트까지의 Compress/Rake tree들을 splay한다. - Splice :
부터 루트까지의 경로에 여러번의 splice 연산을 수행하여 가 가장 위의 Compress tree에 포함되게 만든다. (루트 경로에 포함되게 만든다.) - Global Splay :
를 splay해 전체 Top tree의 루트로 만든다.
1번 과정을 더 자세히 설명하면 다음과 같다.
- 현재 탐색하고 있는 Top tree의 노드를
이라 하자. (초기에는 ) 을 splay하여 이 포함된 Compress tree의 루트로 만든다. 이 Top tree의 루트라면 과정이 종료되고, 아니라면 은 어떤 Rake tree의 리프가 된다. 의 부모가 Rake 노드일 경우 : 의 부모를 를 splay한다.- splay 후
의 부모가 가 아니라면, 를 guard로 하여 의 새 부모 을 splay한다.
을 에 가장 가까운 조상 Compress 노드로 바꾸고 과정을 반복한다.
이 과정 이후
이제
: 가 이므로 해야 할 것이 없다. : 가 리프 : 에 에 했던 것과 동일한 루팅 과정을 수행한다. 이 때 가 루트 정점이여야 한다. 이 경우 가 루트가 된다. 가 리프가 아님 : 에 에 했던 것과 동일한 루팅 과정을 수행하지만, 모든 splay 과정을 를 guard로 하는 guarded splay로 수행한다.
마지막으로, 구현의 단순화를 위해 경로
2.
Cut
Cut 연산은 원래 트리에서 간선 하나를 제거하는 연산이다. 제거할 간선을
먼저

위 그림은 가장 일반적인 경우(
Link
Link 연산은 포레스트에 간선 하나을 추가하는 연산이다. 추가할 간선을

Lazy Propagation
Top tree에서도 Lazy Propagation이 가능하다. Compress tree가 경로를 관리하고, Rake tree가 나머지 서브트리를 관리한다. 따라서 경로에 Lazy를 전파하고 싶을 때는 proper child에만 전파하고, 서브트리에 Lazy를 전파하고 싶을 때는 proper child와 foster child에 모두 전파하면 된다. 어떤 노드에 접근할 때 조상들의 Lazy가 모두 전파되었는지 확인하자.
응용
여기까지 Top tree의 모든 기본 연산들을 살펴보았다. 이제 실제로 Top tree를 사용하여 풀 수 있는 문제들을 살펴보자. 아래 문제들은 모두 간선의 추가, 제거 쿼리가 있는 포레스트에서 해결한다고 생각한다.
문제. 두 정점
각 노드마다 경로의 합
문제. 두 정점
Top tree는 간선을 관리하기 때문에, 정점 쿼리를 지원하기 위해서는 따로 처리를 해주어야 한다. 이는 구현에 따라 다양한 방법이 있을 수 있다. 여기서는 정점 쿼리를 위한 두 가지 방법을 소개한다.
방법 1.
모든 정점에 더미 정점 하나를 연결해 준다. 이러면 원래 있던 정점의 Handle은 모두 Compress 노드가 된다. 그 Compress 노드에 정점 정보를 저장해 주면 된다. 경로 쿼리가 들어올 때는
방법 2.
각 노드마다 경로에 있는 정점의 가중치 합
2번째 방법이 더 효율적이긴 하나, 문제에 따라 적용하기 어려울 수 있다. 물론 이 두 방법만 있는 것은 아니므로 문제에 따라 잘 생각해서 처리하자.
문제. 쿼리마다 포레스트의 간선
(Dynamic Tree Subtree Add Subtree Sum)
이 문제를 해결하기 위해서는 특정 서브트리를 추출해야 한다. 먼저
서브트리에 특정 값을 더하는 연산의 경우 Lazy Propagation이 필요해진다. 위에서 설명했듯이 proper child와 foster child에 모두 Lazy를 전파해주자.
문제. 트리의 지름의 길이를 출력하라.
각 노드마다 경로의 길이, 지름의 길이, 양 끝점에서 가장 먼 정점과의 거리를 관리한다. Rake 노드의 경우, 가장 가까운 조상 Compress 노드를
Rake와 Compress 노드 모두 자식 클러스터가 공유하는 정점이 딱 하나 존재하므로 자식 노드에서 고려되지 않은 경로는 이 정점을 지나는 경로밖에 없다. 이 정점을 지나는 가장 긴 경로는 자식들의 값들을 통해 계산할 수 있다. 이제 각 클러스터의 지름을 구할 수 있으므로, 루트 노드의 지름을 출력하면 된다. 자세한 상태전이 식은 생략한다.
문제. 트리의 1-median을 구하라.
트리의 1-median이란,
Lemma. 트리
이 Lemma를 이용해서, 루트 노드의 두 자식 클러스터중에 어느 쪽에 1-median이 있는지 알 수 있다. 그러므로 Top tree에서 이분 탐색을 하듯이 탐색해나갈 수 있다. 하지만 이 Lemma는 루트 클러스터에서만 적용되므로, 루트 노드 이외의 노드에서는 두 자식만 봐서는 Lemma를 적용할 수 없다. 클러스터는 외부와 연결된 정점이 2개 이하이므로, 그 정점들과 연결된 외부 서브트리의

트리와 쿼리 20 에서는
(wikipedia랑 구사과님의 블로그에는 Top tree를 직접 수정하는 방식으로 Non-local search가 설명되어 있는데, 사실 수정하는 방법을 잘 모르겠다..)
참고 자료
https://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.87.9754&rep=rep1&type=pdf
http://www.secmem.org/blog/2021/03/21/toptree/
https://di.ku.dk/forskning/Publikationer/tekniske_rapporter/tekniske-rapporter-1998/98-17.pdf
https://en.wikipedia.org/wiki/Top_tree
'알고리즘' 카테고리의 다른 글
Online Dynamic Connectivity (동적 연결성 문제 온라인으로 풀기) (0) | 2021.05.17 |
---|---|
EERTREE (Palindromic Tree) (0) | 2021.04.11 |
홀의 결혼 정리 (Hall's marriage theorem)와 PS에서의 응용 (1) | 2021.03.28 |