|
| 1 | +--- |
| 2 | +layout: post |
| 3 | +title: Link Cut Tree Subtree Query |
| 4 | +date: 2025-08-26 04:30 |
| 5 | +author: jthis |
| 6 | +tags: |
| 7 | + - algorithm |
| 8 | + - data-structure |
| 9 | +--- |
| 10 | + |
| 11 | +안녕하세요, **jthis**입니다. |
| 12 | + |
| 13 | +이 글에서는 **Link-Cut Tree**에서 **Subtree Query**를 효율적으로 수행하는 방법에 대해 소개하겠습니다. 편의상 코드는 **Splay Tree**를 기준으로 설명하겠습니다. |
| 14 | + |
| 15 | +# Link Cut Tree |
| 16 | + |
| 17 | +Link-Cut Tree는 **간선 추가 삭제**와 **경로 쿼리(Path Query)** 를 지원하는 자료구조입니다. 시간복잡도는 *amortized* $O(\log N)$이며, **amortized를 제거하여** $O(\log N)$으로도 가능합니다. 또한, 이 자료구조는 **Subtree 연산**도 지원할 수 있습니다. 따라서 Top Tree로 해결할 수 있는 대부분의 쿼리 문제를 거의 모두 **Link-Cut Tree**로 대체할 수 있습니다. |
| 18 | + |
| 19 | +Link-Cut Tree는 Splay Tree로만 구현할 수 있다고 생각할 수도 있지만, 다른 BBST(Balanced Binary Search Tree)로도 충분히 구현할 수 있습니다. BBST에서 **split**과 **merge** 연산만 빠르게 가능하다면, 어떤 BBST라도 Link-Cut Tree를 구성할 수 있습니다. |
| 20 | + |
| 21 | +Link-Cut Tree는 트리를 **경로(Path)** 단위로 여러 개로 쪼개어 관리하는 자료구조입니다. 각 경로는 BBST로 관리하며, 경로에 포함되지 않은 간선은 **가상 간선**으로 생각하여 BBST와는 별도로 관리합니다. |
| 22 | +Link-Cut Tree에서는 **access 연산**과 **makeRoot 연산**만 구현하면 됩니다. |
| 23 | + |
| 24 | +**access 연산**은 트리 상의 루트와 특정 노드 $x$를 하나의 경로로 만드는 연산입니다. 이 연산을 통해 루트와 $x$ 사이에 있는 가상 간선을 실제 간선으로 바꾸고, 그 간선에서 부모 쪽과 연결되어 있던 자식과의 연결을 가상 간선으로 바꿉니다. |
| 25 | + |
| 26 | +노드 $a$에서 access 연산을 수행한 뒤, 노드 $b$에서 access 연산을 수행했을 때, **마지막으로 바뀐 가상 간선, 즉 루트와 가장 가까운 가상 간선의 부모 쪽이 $a$와 $b$의 LCA**가 됩니다. |
| 27 | + |
| 28 | +또한, access $x$ 연산을 수행한 뒤 해당 경로에 **reverse 연산**을 적용하면, 노드 $x$가 트리의 루트가 됩니다. |
| 29 | + |
| 30 | +마지막으로, 간선을 추가하는 **Link** 연산을 위해서는 **자식 쪽을 트리에서 루트로 만들어야 합니다**. |
| 31 | + |
| 32 | +```c++ |
| 33 | +node *access(node *x) { |
| 34 | + splay(x); |
| 35 | + x->r = nullptr; |
| 36 | + node *res = x; |
| 37 | + while (x->p) { |
| 38 | + node *p = x->p; |
| 39 | + res = p; |
| 40 | + splay(p); |
| 41 | + p->r = x; |
| 42 | + splay(x); |
| 43 | + } |
| 44 | + return res; |
| 45 | +} |
| 46 | + |
| 47 | +void makeRoot(node *x) { |
| 48 | + access(x); |
| 49 | + splay_reverse(x); |
| 50 | +} |
| 51 | +``` |
| 52 | +
|
| 53 | +Link-Cut Tree는 간단히 말해 **BBST 사이의 가상 간선을 효율적으로 관리하는 자료구조**입니다. |
| 54 | +
|
| 55 | +# Link Cut Tree Path Query |
| 56 | +
|
| 57 | +두 노드 $a$와 $b$에서 **Path Query**를 수행하기 위해서는 *makeRoot* 연산 없이도 처리할 수 있지만, **makeRoot 연산을 사용하면 훨씬 간단하게 해결할 수 있습니다**. |
| 58 | +노드 $a$에서 makeRoot 연산을 수행한 뒤, 노드 $b$에서 access 연산을 실행하면, $a$-$b$ 경로만 BBST에 남게 되어 문제를 간단하게 해결할 수 있습니다. |
| 59 | +
|
| 60 | +```c++ |
| 61 | +long long pathQuery(node *a, node *b) { |
| 62 | + makeRoot(a); |
| 63 | + access(b); |
| 64 | + return b->pathSum; |
| 65 | +} |
| 66 | +``` |
| 67 | +# Link Cut Tree Subtree Query |
| 68 | + |
| 69 | +**Subtree 연산**을 수행하기 위해서는 **가상 간선**을 적절히 관리하면 됩니다. 가상 간선은 **access 연산**에서만 변경됩니다. |
| 70 | +Subtree에서의 **min**, **max** 연산은 가상 간선을 BBST로 관리하면 해결할 수 있으며, **sum** 연산이 필요하다면 구조체에 `int` 하나를 추가하면 됩니다. |
| 71 | +또한, access 연산에서는 **min**, **max** 값은 삽입과 삭제(insert, delete)만 수행하면 되고, **sum**은 차이만 계산하여 누적하면 됩니다. |
| 72 | + |
| 73 | +```c++ |
| 74 | +void update(node *x) { |
| 75 | + x->mx = x->val; |
| 76 | + if (!x->vs.empty())x->mx = max(x->mx, *prev(x->vs.end())); |
| 77 | + if (x->l)x->mx = max(x->mx, x->l->mx); |
| 78 | + if (x->r)x->mx = max(x->mx, x->r->mx); |
| 79 | +} |
| 80 | +node *access(node *x) { |
| 81 | + splay(x); |
| 82 | + if (x->r)x->vs.insert(x->r->mx); |
| 83 | + x->r = nullptr; |
| 84 | + node *res = x; |
| 85 | + while (x->p) { |
| 86 | + node *p = x->p; |
| 87 | + res = p; |
| 88 | + splay(p); |
| 89 | + if (p->r)p->vs.insert(p->r->mx); |
| 90 | + p->vs.erase(p->vs.find(x->mx)); |
| 91 | + p->r = x; |
| 92 | + splay(x); |
| 93 | + } |
| 94 | + return res; |
| 95 | +} |
| 96 | +``` |
| 97 | +
|
| 98 | +**Subtree에서 최대값 연산**의 구현입니다. 구현의 편의를 위해 `multiset`을 사용했습니다. |
| 99 | +# Link Cut Tree Subtree Update |
| 100 | +### Subtree add |
| 101 | +```c++ |
| 102 | +void rotate(node *x) { |
| 103 | + node *p = x->p; |
| 104 | + if (x == p->l) { |
| 105 | + p->l = x->r; |
| 106 | + if (p->l) { |
| 107 | + p->l->p = p; |
| 108 | + p->l->gets = p->added; |
| 109 | + } |
| 110 | + x->r = p; |
| 111 | + } else { |
| 112 | + p->r = x->l; |
| 113 | + if (p->r) { |
| 114 | + p->r->p = p; |
| 115 | + p->r->gets = p->added; |
| 116 | + } |
| 117 | + x->l = p; |
| 118 | + } |
| 119 | + x->p = p->p; |
| 120 | + if (x->p)x->gets = x->p->added; |
| 121 | + p->p = x; |
| 122 | + p->gets = x->added; |
| 123 | + if (x->p) { |
| 124 | + if (p == x->p->l)x->p->l = x; |
| 125 | + else if (p == x->p->r)x->p->r = x; |
| 126 | + } |
| 127 | +} |
| 128 | +
|
| 129 | +void adding(node *x, int diff) { |
| 130 | + x->sum += diff * x->sz; |
| 131 | + x->now += diff; |
| 132 | + x->added += diff; |
| 133 | + x->vsum += diff * x->vsz; |
| 134 | +} |
| 135 | + |
| 136 | +void lazy_down(node *x) { |
| 137 | + if (x->p) { |
| 138 | + adding(x, x->p->added - x->gets); |
| 139 | + x->gets = x->p->added; |
| 140 | + } |
| 141 | +} |
| 142 | +``` |
| 143 | + |
| 144 | +`added`와 `gets`라는 변수를 유지합니다. |
| 145 | + |
| 146 | +* **added**: 해당 Subtree에 더해야 할 값의 누적 합 |
| 147 | +* **gets**: 지금까지 부모로부터 받은 누적 합 |
| 148 | + |
| 149 | +`rotate` 함수는 부모와의 연결을 수정하기 때문에, 이 함수도 함께 수정해야 합니다. 또한 **Subtree 크기(size)** 도 유지해야 합니다. |
| 150 | + |
| 151 | +--- |
| 152 | + |
| 153 | +### Subtree 변경 (Subtree Change) |
| 154 | + |
| 155 | +`add` 연산과 유사하게 구현할 수 있습니다. 다만 값 자체를 저장하는 대신 **쿼리 인덱스**를 저장하여, 부모와 내가 다를 경우에만 업데이트하는 방식으로 처리합니다. |
| 156 | + |
| 157 | + |
| 158 | +# 예시 문제 |
| 159 | + |
| 160 | +### **1. [백준 14268 - 회사 문화 2](https://www.acmicpc.net/problem/14268)** |
| 161 | + |
| 162 | +* **요구사항**: Subtree에 대한 `add` 업데이트와 특정 노드의 값 출력 |
| 163 | +* **예시 코드**: [소스 링크](http://boj.kr/ef9fdd7e02954fedbd7c66c022d9c641) |
| 164 | + |
| 165 | +--- |
| 166 | + |
| 167 | +### **2. [백준 13515 - 트리와 쿼리 6](https://www.acmicpc.net/problem/13515)** |
| 168 | + |
| 169 | +* **요구사항**: 색깔(흰색/검정색)에 따라 연결 관리 |
| 170 | +* **아이디어**: |
| 171 | + * **포레스트 2개**를 만든다 (0번 트리, 1번 트리) |
| 172 | + * 정점 `x`가 흰색이면 `x`와 `x`의 부모는 **트리 0번**에서만 연결 |
| 173 | + * 정점 `x`가 검정색이면 `x`와 `x`의 부모는 **트리 1번**에서만 연결 |
| 174 | + * **쿼리 1**: |
| 175 | + * 간선 추가 및 간선 삭제 |
| 176 | + * **쿼리 2**: |
| 177 | + * u의 색깔에 맞는 트리에서 탐색 |
| 178 | + * 두 가지 경우 |
| 179 | + 1. **트리 전체가 같은 색** → 제거 없이 `subtree size` |
| 180 | + 2. **트리 중 루트만 색 다름** → 루트와의 간선 제거 후 `subtree size` |
| 181 | +* **예시 코드**: [소스 링크](https://www.acmicpc.net/source/share/a5dff557dde94d048b88e7e590bcce95) |
| 182 | + |
| 183 | +--- |
| 184 | + |
| 185 | +### **3. [백준 13516 - 트리와 쿼리 7](https://www.acmicpc.net/problem/13516)** |
| 186 | + |
| 187 | +* **요구사항**: Subtree에서 최대값 구하기 |
| 188 | +* **아이디어**: |
| 189 | + * 위 문제(13515)와 거의 동일 |
| 190 | + * 차이점 → `subtree size` 대신 `subtree max` 계산 |
| 191 | +* **예시 코드**: [소스 링크](http://boj.kr/1576e635eac9445c9a6f6bed8f00647e) |
| 192 | + |
| 193 | +--- |
| 194 | + |
| 195 | +### **4. [백준 18805 - Tree and Easy Queries](https://www.acmicpc.net/problem/18805)** |
| 196 | + |
| 197 | +* **요구사항**: 지름(Diameter) 구하기 |
| 198 | +* **아이디어**: |
| 199 | + * 동적 트리에서 지름 유지 |
| 200 | +* **예시 코드**: [소스 링크](http://boj.kr/4a7037d27f994aafb2599fa91b494097) |
| 201 | + |
| 202 | +--- |
| 203 | + |
| 204 | +### **5. [백준 10014 - Traveling Saga Problem](https://www.acmicpc.net/problem/10014)** |
| 205 | + |
| 206 | +* **요구사항**: 지름(Diameter) 구하기 |
| 207 | +* **아이디어**: |
| 208 | + * 위 문제(18805)와 동일한 방식 |
| 209 | +* **예시 코드**: [소스 링크](http://boj.kr/6df8dbc3230c472ba3f733c621545e1d) |
| 210 | + |
| 211 | +--- |
| 212 | + |
| 213 | +# Dynamic Tree 비교 |
| 214 | + |
| 215 | +| 구분 | Fragmented Tree | Euler Tour Tree | Link-Cut Tree | Top Tree | |
| 216 | +|---------------|-----------------|-----------------|--------------|-----------| |
| 217 | +| Path Query | 가능 | 제한적 | 가능 | 가능 | |
| 218 | +| Subtree Query | 가능 | 가능 | 가능 | 가능 | |
| 219 | +| 시간 복잡도 | O(√N) | O(log N) | O(log N) | O(log N) | |
| 220 | +| 구현 난이도 | 중간 | 비교적 쉬움 | 중간 | 어려움 | |
| 221 | + |
| 222 | +# 응용 |
| 223 | + |
| 224 | +Link-Cut Tree는 **경로 쿼리(Path Query)** 와 **Subtree 쿼리**를 사용하여 다양하게 응용할 수 있습니다. |
| 225 | +* **트리 지름(Diameter)** |
| 226 | +* **Rerooting DP** |
| 227 | +* **Tree DP** |
| 228 | +* **Dynamic Centroid** |
| 229 | +* **Online LCA** |
| 230 | +* **Subtree Size 계산** |
| 231 | +* 기타 동적 트리 관련 쿼리 |
| 232 | + |
| 233 | +--- |
| 234 | + |
| 235 | +# 실전 문제 |
| 236 | + |
| 237 | +* [백준 17936 - 트리와 쿼리 13](https://www.acmicpc.net/problem/17936) |
| 238 | +* [백준 31705 - Kolorowy las](https://www.acmicpc.net/problem/31705) |
| 239 | +* [백준 1921 - 트리와 쿼리 20](https://www.acmicpc.net/problem/1921) |
| 240 | +* [백준 26408 - Game](https://www.acmicpc.net/problem/26408) |
| 241 | +* [Luogu P5610](https://www.luogu.com.cn/problem/P5610) |
| 242 | +* [Link-Cut Tree 문제집](https://www.acmicpc.net/workbook/view/7004) |
0 commit comments