본문으로 건너뛰기
김신건의 로그

오일러 투어 테크닉 (Euler Tour Technique)

· 수정 · 📖 약 3분 · 992자/단어 #algorithm #tree #euler-tour #flattening #range-query
euler tour, 오일러 투어, ETT, 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";
  }
}
stdin
5 3
1 2 3 4 5
1 2
1 3
2 4
2 5
1
2
3
결과
15
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 연습 문제

번호제목정답률링크
BOJ 13511트리와 쿼리 2-kokoa-lab
BOJ 15681트리와 쿼리-kokoa-lab
BOJ 17435합성함수와 쿼리-kokoa-lab
BOJ 3745오름세-kokoa-lab

참고

이 글의 용어 (6개)
세그먼트 트리 (Segment Tree)algorithm
정의 세그먼트 트리 (Segment Tree) 는 배열의 구간 쿼리 (range query) 와 점 갱신 (point update) 를 모두 O(log N) 에 처리하는 이진 트…
최소 공통 조상 (Lowest Common Ancestor)algorithm
정의 최소 공통 조상 (LCA, Lowest Common Ancestor) 는 트리에서 두 노드 u, v 의 공통 조상 중 가장 깊은 (루트에서 가장 먼) 노드. Binary L…
희소 배열 (Sparse Table)algorithm
정의 희소 배열 (Sparse Table) 은 정적 배열에서 결합 법칙을 만족하는 idempotent 연산 (min, max, gcd, lcm 등) 의 구간 쿼리를 O(1) 시간…
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 경로의 합/최대/최소) 를 …

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기