오일러 투어 테크닉 (Euler Tour Technique)
정의
오일러 투어 테크닉 (ETT, Euler Tour Technique) 은 트리를 DFS 방문 순서로 펼쳐 서브트리 쿼리를 구간 쿼리로 변환하는 정형. 각 노드 u 의 in-time tin[u], out-time tout[u] 를 기록하면, u 의 서브트리 = 구간 [tin[u], tout[u]] 로 표현되어 세그먼트 트리, 펜윅 트리 등 구간 자료구조에서 O(log N) 쿼리.
문제 상황과 동기
트리에서 “u 의 서브트리에 속한 모든 노드의 합 / 최댓값 / 개수” 를 Q 번 묻는다.
- naive: 매 쿼리마다 서브트리 전체 DFS. O(N · Q). N=Q=10^5 면 10^10.
- ETT + Segtree: O(N) 전처리 + O(log N) 쿼리. 총 O(N + Q log N).
핵심 통찰: 트리의 서브트리는 DFS 방문 순서에서 연속 구간. 따라서 트리 문제를 배열 문제로 환원.
시각화
핵심 아이디어
invariant: DFS 방문 순서대로 노드를 나열하면, u 의 서브트리 = [tin[u], tout[u]].
tin[u] = DFS 진입 시각
tout[u] = DFS 퇴각 시각
is_ancestor(u, v) = tin[u] <= tin[v] && tout[v] <= tout[u]
subtree_range(u) = [tin[u], tout[u]]
확장:
- 서브트리 갱신: 구간 갱신 (lazy propagation).
- 경로 쿼리 + HLD: HLD 와 결합하면 경로도 O(log^2 N).
- LCA + RMQ: 오일러 투어 순서 위에 depth 배열로 LCA 를 O(1) RMQ 로.
알고리즘
dfs(u, parent):
tin[u] = timer++
euler[timer - 1] = u # in-time 에 노드 기록
for v in adj[u]:
if v != parent:
dfs(v, u)
tout[u] = timer++
euler[timer - 1] = u # out-time 에 노드 기록 (선택)
build_euler_tour(root):
timer = 0
dfs(root, -1)
subtree_query(u):
# u 의 서브트리 노드들은 euler[tin[u]..tout[u]] 에 모두 포함
return range_query(segtree, tin[u], tout[u])
구현
// Euler Tour + 서브트리 합 쿼리 (Fenwick)
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1e5;
vector<int> adj[MAXN];
int tin[MAXN], tout[MAXN], timer = 0;
long long val[MAXN], fenwick[MAXN * 2 + 5];
int N;
void update(int i, long long delta) {
for (; i <= 2 * N; i += i & -i) fenwick[i] += delta;
}
long long query(int i) {
long long res = 0;
for (; i > 0; i -= i & -i) res += fenwick[i];
return res;
}
void dfs(int u, int p) {
tin[u] = ++timer;
update(tin[u], val[u]);
for (int v : adj[u]) if (v != p) dfs(v, u);
tout[u] = timer;
}
int main() {
int Q; cin >> N >> Q;
for (int i = 0; i < N; i++) cin >> val[i];
for (int i = 0; i < N - 1; i++) {
int a, b; cin >> a >> b; a--; b--;
adj[a].push_back(b); adj[b].push_back(a);
}
dfs(0, -1);
while (Q--) {
int u; cin >> u; u--;
cout << query(tout[u]) - query(tin[u] - 1) << "\n";
}
}5 3
1 2 3 4 5
1 2
1 3
2 4
2 5
1
2
315
11
3복잡도
| 항목 | 값 |
|---|---|
| 전처리 (DFS) | O(N) 시간, O(N) 공간 |
| 쿼리 (Segtree/Fenwick) | O(log N) 시간 |
| 전체 | O(N + Q log N) |
변형 / 활용
| 응용 | 설명 |
|---|---|
| 서브트리 갱신 | 구간 갱신 (lazy propagation) |
| LCA via RMQ | 오일러 투어 순서에 depth 배열 → [[Sparse Table |
| 경로 쿼리 + HLD | [[Heavy-Light Decomposition |
| 동적 트리 | [[Link-Cut Tree |
| 재귀 없는 DFS | 스택으로 in/out 시각 기록 가능 |
함정
1. in-time / out-time 혼동
tin[u] 는 DFS 진입, tout[u] 는 퇴각. 서브트리 구간은 [tin[u], tout[u]] (inclusive).
2. 0-indexed vs 1-indexed
Fenwick / Segtree 가 1-indexed 면 tin, tout 도 1-indexed 로 맞춰야. Python 은 0-indexed 가 자연스러움.
3. 서브트리만 가능, 경로는 HLD 필요
ETT 단독으로는 서브트리 쿼리만 O(log N). 경로 쿼리는 HLD 필요.
4. out-time 에 노드 중복 기록?
구현에 따라 euler 배열에 노드를 in/out 두 번 넣거나, in 만 넣거나. 서브트리 크기 계산 시 주의.
BOJ 예시 문제 풀이
Euler tour 를 실전에서 어떻게 활용하는지 4개의 대표 문제를 풀이 흐름으로 정리합니다. 원문 링크는 kokoa-lab boj-problems 저장소에서 확인할 수 있습니다.
BOJ 15681: 트리와 쿼리
문제 요약: 루트 이 주어진 트리에서 각 정점 를 루트로 하는 서브트리의 노드 개수를 여러 번 묻습니다.
- N ≤ 10^5, Q ≤ 10^5
전형적 ETT. 서브트리 크기 자체가 답이므로 세그먼트 트리도 불필요.
풀이 흐름:
- 루트에서 DFS 를 돌며
tin[u],tout[u]를 기록. - 서브트리 크기 =
tout[u] - tin[u] + 1(in-time 만 카운트하는 경우) 또는 DFS 중 자식 크기 합. - 쿼리마다 의 서브트리 크기 반환.
dfs(u, parent):
tin[u] = timer++
size[u] = 1
for v in adj[u]:
if v != parent:
dfs(v, u)
size[u] += size[v]
tout[u] = timer
query(u): return size[u]
시간 복잡도: 전처리 O(N), 쿼리 O(1). 매우 단순.
교훈: ETT 의 in/out-time 없이도 서브트리 크기 자체는 자식 크기 합으로 계산. 하지만 tin, tout 을 저장해두면 범위 쿼리 로 확장 가능하다는 점이 핵심.
확장 (합 버전): 각 정점에 값이 있다면 Fenwick 로 [tin[u], tout[u]] 구간 합 = 서브트리 합.
- BOJ 원문: BOJ 15681
BOJ 13511: 트리와 쿼리 2
문제 요약: 가중치 트리에서 두 정점 사이 경로 위:
- 간선 가중치의 합
- 경로 위 번째 정점
- N ≤ 10^5, Q ≤ 10^5
ETT 단독으로는 부족. 경로 쿼리는 LCA + prefix sum (또는 HLD) 필요.
풀이 흐름:
- DFS 로
depth[u],distFromRoot[u](루트에서 u 까지 간선 가중치 합) 계산. 동시에tin[u]를 기록해 오일러 투어 순서 확보. - Binary Lifting 으로
up[u][i](2^i 조상) 테이블 구축. - 쿼리 1 (경로 합): 자세한 것은 Path sum on tree 참조 (간선 가중치이므로 LCA 값 보정 없음).
- 쿼리 2 (k번째 정점):
- 방향의 정점 수:
- 가 이 개수 이하 -> 의 번째 조상
- 아니면 의 번째 조상 (즉 반대 방향)
- 조상 찾기는 K번째 조상.
교훈: 경로 문제는 ETT 만으로 불가능. ETT + Binary Lifting + LCA 조합이 정통. HLD 도 대안이지만 구현 부담.
- BOJ 원문: BOJ 13511
BOJ 17435: 합성함수와 쿼리
문제 요약: 함수 가 주어지고, 쿼리로 에 대해 (함수 번 합성 결과) 를 구합니다.
- m ≤ 200000, Q ≤ 200000, n ≤ 500000
엄밀히는 트리 문제 아님. 함수의 반복 적용은 functional graph 에서 다음 정점 따라가기와 동일. Binary lifting 이 그대로 적용.
풀이 흐름:
up[x][0] = f(x)(1번 적용).up[x][i] = up[up[x][i-1]][i-1](재귀 doubling).- 쿼리 : 의 이진 표현대로 점프.
이는 트리 아닌 binary lifting 응용의 대표 예. Euler tour 와의 직접 연관은 약하지만, 트리 문제에서 doubling 사고를 배웠다면 자연스럽게 이 문제도 풀리는 관점이 형성됩니다.
교훈: doubling 은 트리에만 국한되지 않음. 결정적 이산 동역학 (deterministic discrete dynamics) 에 모두 적용 가능.
- BOJ 원문: BOJ 17435
BOJ 3745: 오름세 (LIS)
문제 요약: 수열의 최장 증가 부분수열 (LIS) 길이.
- N ≤ 200000
Euler tour 와 무관. 하지만 kokoa-lab 저장소 에서 함께 태그된 이유는 아마도:
- 패치 트리 이용 LIS: 트리 구조를 활용한 LIS 변형 문제군
- Fenwick 로 LIS:
dp[v] = 1 + max(dp[u] : u < v && val[u] < val[v])를 Fenwick 로 O(N log N).
트리 자료구조 (Fenwick) 를 이용하지만 Euler tour 의 in/out-time 개념은 사용하지 않습니다.
풀이 흐름 (표준 LIS):
- 값 좌표압축.
- Fenwick 을 값 도메인에 만들고
bit[v]= “지금까지 값 v 로 끝나는 LIS 최댓값”. - 각 원소 에 대해
dp[i] = 1 + query(1..a_i - 1), 그 후update(a_i, dp[i]).
교훈: “트리 관련 자료구조” 문제와 “트리 위의 문제” 는 다름. Fenwick / Segment tree 는 배열 위에서도 강력한 도구.
- BOJ 원문: BOJ 3745
참고
이 글의 용어 (9개)
- 세그먼트 트리 (Segment Tree)algorithm
- 정의 세그먼트 트리 (Segment Tree) 는 배열의 구간 쿼리 (range query) 와 점 갱신 (point update) 를 모두 O(log N) 에 처리하는 이진 트…
- 최소 공통 조상 (Lowest Common Ancestor)algorithm
- 정의 최소 공통 조상 (LCA, Lowest Common Ancestor) 는 트리에서 두 노드 u, v 의 공통 조상 중 가장 깊은 (루트에서 가장 먼) 노드. Binary L…
- 트리 경로 합 (Path Sum on Tree)algorithm
- 정의 트리 경로 합 (Path Sum on Tree) 는 트리에서 두 정점 $u, v$ 사이의 유일 경로 위 정점 (또는 간선) 값의 합 을 구하는 문제입니다. 문제 상황은 크게…
- 희소 배열 (Sparse Table)algorithm
- 정의 희소 배열 (Sparse Table) 은 정적 배열에서 결합 법칙을 만족하는 idempotent 연산 (min, max, gcd, lcm 등) 의 구간 쿼리를 O(1) 시간…
- Binary Lifting: 2의 지수 doublingalgorithm
- 정의 Binary Lifting (이진 상승, doubling) 은 트리 (또는 functional graph) 에서 정점의 $2^k$ 번째 조상 을 미리 계산해 두어 임의의 $…
- Dynamic Tree (Link/Cut Tree, Euler Tour Tree, Top Tree)algorithm
- 정의 Dynamic Tree 는 트리에 간선 추가 (link) / 제거 (cut) 가 섞이는 환경에서 경로 / 서브트리 집계 쿼리 를 O(log N) 에 처리하는 자료구조 가족.…
- Fenwick Tree (Binary Indexed Tree): 구간 합 O(log N)algorithm
- 정의 Fenwick Tree (또는 BIT, Binary Indexed Tree) 는 배열의 prefix sum 을 O(log N) 에 갱신·조회 하는 자료구조입니다. Peter…
- Heavy-Light Decompositionalgorithm
- 정의 Heavy-Light Decomposition (HLD) 는 트리를 O(log N) 개의 경로 (chain) 로 분해해 임의 경로 쿼리 (u-v 경로의 합/최대/최소) 를 …
- K번째 조상 (K-th Ancestor)algorithm
- 정의 K번째 조상 문제 (K-th ancestor) 는 루트 트리에서 정점 $u$ 의 조상 중 $u$ 로부터 정확히 $k$ 만큼 위에 있는 정점 (즉, $u$ 의 $k$ 번째 부…
💬 댓글