K번째 조상 (K-th Ancestor)
정의
K번째 조상 문제 (K-th ancestor) 는 루트 트리에서 정점 의 조상 중 로부터 정확히 만큼 위에 있는 정점 (즉, 의 번째 부모) 을 찾는 문제입니다.
정의:
\text\{kthAncestor\}(u, k) = \begin\{cases\} u & (k = 0) \\ \text\{parent\}(u) & (k = 1) \\ \text\{kthAncestor\}(\text\{parent\}(u), k - 1) & (k \geq 2) \end\{cases\}가 의 깊이보다 크면 조상이 없음 (일반적으로 반환).
왜 어려운가
Naive: 부모를 한 번씩 따라 올라감. O(k) per query. 이면 총 O(NQ) = 로 안 됨.
핵심 트릭은 Binary Lifting 으로 번째 조상 테이블을 O(N log N) 시간에 미리 계산하고, k 를 이진 표현으로 분해하여 O(log N) 안에 답합니다.
알고리즘: Binary Lifting
아이디어
를 이진법으로 분해합니다.
각 비트 만큼 위로 올라가는 것을 미리 저장 해두면, set bits 만 순회하며 조상을 찾을 수 있습니다.
예:
에서 시작해:
- 번째 조상으로 이동
- 번째 조상으로 이동
- 번째 조상으로 이동
총 3번 점프로 13번째 조상 도달.
왜 이게 가능한가
관건은 번째 조상 = 번째 조상의 번째 조상 이라는 재귀 구조.
이 함성 관계로 전처리.
자세한 시각화는 Binary Lifting 참조.
시각화: k = 13 조상 찾기
flowchart LR U["u<br/>depth=20"] -->|"2^0 = 1 step"| A1["depth=19"] A1 -->|"2^2 = 4 steps"| A2["depth=15"] A2 -->|"2^3 = 8 steps"| A3["depth=7<br/>= 13번째 조상"] style U fill:#fef3c7 style A3 fill:#a7f3d0
한 번의 O(log k) 순회로 13 = 1 + 4 + 8 을 조합.
알고리즘 (의사코드)
전처리 (트리 DFS):
up[u][0] = parent[u]
for i in 1..LOG:
up[u][i] = up[up[u][i-1]][i-1]
쿼리 kthAncestor(u, k):
for i in 0..LOG:
if k & (1 << i):
u = up[u][i]
if u == -1: return -1
return u
LOG = ceil(log2(N)). 이면 LOG = 17 로 충분.
구현
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 500005;
const int LOG = 20;
int up[MAXN][LOG];
int depth[MAXN];
vector<int> adj[MAXN];
void dfs(int u, int p) {
up[u][0] = p;
for (int i = 1; i < LOG; i++) {
up[u][i] = up[u][i-1] < 0 ? -1 : up[up[u][i-1]][i-1];
}
for (int v : adj[u]) if (v != p) {
depth[v] = depth[u] + 1;
dfs(v, u);
}
}
int kthAncestor(int u, int k) {
for (int i = 0; i < LOG; i++) {
if (k & (1 << i)) {
u = up[u][i];
if (u < 0) return -1;
}
}
return u;
}
int main() {
int n, q;
cin >> n;
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);
}
depth[0] = 0;
dfs(0, -1);
cin >> q;
while (q--) {
int u, k;
cin >> u >> k;
u--;
int ans = kthAncestor(u, k);
cout << (ans < 0 ? -1 : ans + 1) << "\n";
}
}5
1 2
1 3
2 4
2 5
3
5 2
4 1
5 41
2
-1복잡도
| 항목 | 값 |
|---|---|
| 전처리 | O(N log N) 시간, O(N log N) 공간 |
| 쿼리 | O(log N) |
| 전체 | O((N + Q) log N) |
응용
1. LCA (Lowest Common Ancestor)
의 LCA 를 구할 때:
- 깊이 차이만큼 낮은 쪽을 K번째 조상으로 올림 ().
- 남은 부분은 두 정점 동시에 위로 (top-down doubling).
K번째 조상은 LCA 의 핵심 부품.
2. Path sum on tree
에서 의 번째 조상까지의 경로 위 값 합계. 방문 순서에 따른 부분합 + K번째 조상 위치로 계산.
자세한 것은 Path sum on tree 참조.
3. Level Ancestor (LA)
일반화 문제. 에서 특정 깊이 의 조상 = .
4. Functional Graph K-th successor
트리 대신 각 정점이 하나의 다음 정점을 가리키는 그래프에서 번째 후속자.
같은 doubling 기법. 예: BOJ 17435 합성함수와 쿼리.
5. 문자열 서픽스 링크 (Suffix Automaton)
Suffix automaton 의 link 그래프도 트리 형태. 특정 상태의 번째 조상 상태.
함정
1. LOG 계산
LOG = ceil(log2(N)) + 1 로 여유. 이면 LOG >= 20.
2. Off-by-one
kth(u, 0)=u자신 (조상 아님)kth(u, 1)= 부모- =
-1
3. 재귀 스택 오버플로
이면 Python / C++ 모두 스택 부족 가능. BFS 방식 iterative DFS 사용.
4. Doubling 채우기 순서
up[u][i] = up[up[u][i-1]][i-1] 를 계산할 때 up[?][i-1] 이 이미 채워져 있어야 함. DFS 방문 순서로 채우면 부모가 먼저 처리되므로 안전.
BOJ 연습 문제
| 번호 | 제목 | 링크 |
|---|---|---|
| BOJ 3653 | 영화 수집 | BOJ |
| BOJ 17435 | 합성함수와 쿼리 | BOJ |
| BOJ 3176 | 도로 네트워크 | BOJ |
| BOJ 13511 | 트리와 쿼리 2 | BOJ |
참고
- Binary Lifting - 근간 기법
- LCA (최소 공통 조상) - K번째 조상의 대표 응용
- Path sum on tree - 조상 경로 합
- Euler Tour - 트리 쿼리 대안
- 희소 배열 - 유사한 doubling 아이디어
이 글의 용어 (5개)
- 오일러 투어 테크닉 (Euler Tour Technique)algorithm
- 정의 오일러 투어 테크닉 (ETT, Euler Tour Technique) 은 트리를 DFS 방문 순서로 펼쳐 서브트리 쿼리를 구간 쿼리로 변환하는 정형. 각 노드 u 의 in-…
- 최소 공통 조상 (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$ 번째 조상 을 미리 계산해 두어 임의의 $…
💬 댓글