최소 공통 조상 (LCA, Lowest Common Ancestor)
한눈에 보기
트리에서 두 노드의 가장 깊은 공통 조상을 찾는 LCA 알고리즘을 다룬다. 단순 상승 방법부터 희소 테이블 기반 이진 리프팅까지 원리, 질의 과정, 구현 코드를 정리한다.
최종 업데이트 2026.09.17
최소 공통 조상(LCA, Lowest Common Ancestor)은 트리에서 두 노드의 공통 조상 중 가장 깊은 노드를 말한다. 두 노드가 갈라지기 직전의 갈림길이라고 생각하면 쉽다.
트리에서 두 노드 사이의 거리를 구하거나, 경로 위의 값을 다루는 문제가 대부분 LCA로 환원된다. 질의가 한두 번이면 단순하게 올라가도 되지만, 질의가 10만 번씩 들어오면 희소 배열로 O(log n)에 답해야 한다. 이 글은 그 두 단계를 순서대로 다룬다.
무엇을 구하는 문제인가
가장 단순한 방법
두 노드의 깊이가 다르면 먼저 깊은 쪽을 끌어올려 깊이를 맞춘다. 그다음 둘이 같아질 때까지 한 칸씩 함께 올라간다. 만나는 지점이 LCA다.
깊이를 먼저 맞춰야 하는 이유는 명확하다. 깊이가 다른 상태로 같이 올라가면 서로 다른 층에 있어서 영영 만나지 못하거나, 만나더라도 공통 조상이 아닌 곳에서 마주칠 수 있다.
이 방법은 구현이 쉽고 질의가 적을 때 충분하다. 하지만 한쪽으로 길게 늘어진 트리라면 한 번의 질의에 O(n)이 걸린다. 질의가 n번이면 O(n²)이라 시간 초과가 난다.
희소 배열 — 2의 거듭제곱만큼 점프하기
한 칸씩 올라가는 게 느리다면, 여러 칸을 한 번에 뛰는 지름길을 미리 만들어두면 된다. 이것이 희소 배열(sparse table) 또는 이진 점프(binary lifting)다.
up[k][v]를 "정점 v의 2k번째 조상"으로 정의한다. up[0][v]는 그냥 부모다. 그리고 나머지는 다음 한 줄로 채워진다.
up[k][v] = up[k-1][ up[k-1][v] ]
2k칸 위로 가는 것은 2k-1칸 위로 두 번 가는 것과 같다는 뜻이다. 이미 계산해둔 절반짜리 점프를 두 번 이어 붙여 두 배짜리 점프를 만든다.
1101₂로 보고 2³, 2², 2⁰ 점프를 차례로 하면 된다. 어떤 거리든 최대 log n 번의 점프로 끝나는 이유다.LCA를 구할 때도 같은 요령을 두 번 쓴다.
- 깊이 맞추기: 깊이 차이를 이진수로 쪼개, 해당하는 비트마다 점프한다.
- 갈라지는 지점 찾기: 큰 점프부터 시도하며 두 노드의 조상이 서로 다를 때만 올라간다. 조상이 같아지는 점프는 LCA를 지나쳐 버리는 것이므로 하지 않는다. 이 과정이 끝나면 두 노드는 LCA 바로 아래에 나란히 서 있고, 답은 그 부모다.
시간 복잡도
| 방식 | 전처리 | 질의 1회 | 공간 |
|---|---|---|---|
| 한 칸씩 올라가기 | O(n) | O(n) | O(n) |
| 희소 배열 | O(n log n) | O(log n) | O(n log n) |
질의 개수를 q라 하면 전자는 O(n + qn), 후자는 O(n log n + q log n)이다. n과 q가 각각 10만이면 전자는 100억 번, 후자는 약 340만 번이라 승부가 명확하다.
구현 코드 예시
from collections import deque
LOG = 18 # 2^18 = 262144 > 10만. n에 맞춰 잡는다
def build(n, graph, root=1):
up = [[0] * (n + 1) for _ in range(LOG)]
depth = [0] * (n + 1)
visited = [False] * (n + 1) # BFS로 부모와 깊이를 채운다 (재귀 깊이 걱정 없음)
visited[root] = True
q = deque([root])
while q:
v = q.popleft()
for nxt in graph[v]:
if not visited[nxt]:
visited[nxt] = True
up[0][nxt] = v
depth[nxt] = depth[v] + 1
q.append(nxt)
for k in range(1, LOG): # 절반짜리 점프를 두 번 이어 두 배 점프를 만든다
for v in range(1, n + 1):
up[k][v] = up[k - 1][up[k - 1][v]]
return up, depth
def lca(a, b, up, depth):
if depth[a] < depth[b]: # a를 항상 더 깊은 쪽으로
a, b = b, a
diff = depth[a] - depth[b]
for k in range(LOG): # 깊이 차이를 이진수로 쪼개 점프
if diff & (1 << k):
a = up[k][a]
if a == b: # 한쪽이 다른 쪽의 조상이었던 경우
return a
for k in range(LOG - 1, -1, -1): # 큰 점프부터, 조상이 다를 때만 올라간다
if up[k][a] != up[k][b]:
a = up[k][a]
b = up[k][b]
return up[0][a] # 갈라지기 직전에 멈췄으므로 부모가 답
def distance(a, b, up, depth):
"""트리에서 두 노드 사이의 간선 개수."""
return depth[a] + depth[b] - 2 * depth[lca(a, b, up, depth)]
두 번째 반복문에서 큰 점프부터 시도하는 것이 중요하다. 작은 것부터 하면 LCA를 지나칠 수 있다. 그리고 up[k][a] != up[k][b]일 때만 올라가므로, 절대 LCA를 넘어가지 않고 정확히 그 아래에서 멈춘다.
루트의 부모를 0으로 두고 up[k][0] = 0이 되게 하면, 범위를 벗어난 점프가 0에 머물러 자동으로 안전해진다. 정점 번호를 1부터 쓰는 이유가 이것이다.
LCA로 풀리는 문제들
- 두 노드 사이의 거리:
depth[a] + depth[b] - 2 × depth[lca]. 위 코드의distance가 그것이다. - 경로가 특정 노드를 지나는가:
lca(a, b)가 그 노드이거나, 그 노드가 경로 위에 있는지 깊이로 판별한다. - 경로 위의 최댓값·합:
up과 같은 방식으로maxval[k][v]를 함께 만들어두면 점프하면서 동시에 집계할 수 있다. - 트리에서의 k번째 조상: LCA를 구할 필요 없이 깊이 맞추기 로직만 떼어 쓰면 된다.
자주 하는 실수
- 깊이를 맞추지 않고 같이 올라간다. 서로 다른 층에 있으면 만나는 지점이 공통 조상이라는 보장이 없다.
- 두 번째 단계에서 작은 점프부터 한다. 반드시
range(LOG - 1, -1, -1)로 큰 것부터다. 작은 것부터 하면 LCA를 지나쳐 루트까지 올라가 버린다. a == b검사를 빼먹는다. 한 노드가 다른 노드의 조상인 경우, 깊이를 맞춘 직후 둘이 같아진다. 이때 바로 반환하지 않으면 두 번째 반복문이 엉뚱한 답을 낸다.LOG를 너무 작게 잡는다. 2LOG가 최대 깊이보다 커야 한다. n이 10만이면 17 이상, 넉넉히 20으로 잡으면 안전하다.- DFS 재귀로 부모를 채운다. 일자로 늘어진 트리에서 재귀 깊이 초과가 난다. BFS를 쓰거나 재귀 한도를 올린다.
- 희소 배열 채우는 순서를 뒤집는다.
k바깥,v안쪽이어야 한다.up[k]는up[k-1]이 모두 채워진 뒤에야 계산할 수 있다.
면접에서 이렇게 물어본다
- LCA가 무엇이고 어떤 문제에 쓰이는지 설명해 보라.
- 단순한 방법의 복잡도와 그것이 부족한 상황은 언제인가.
up[k][v] = up[k-1][up[k-1][v]]가 성립하는 이유를 설명해 보라.- 희소 배열로 질의가 O(log n)이 되는 이유는 무엇인가.
- 갈라지는 지점을 찾을 때 큰 점프부터 시도해야 하는 이유는 무엇인가.
- 트리에서 두 노드 사이의 거리를 LCA로 어떻게 구하는가.
- 전처리 비용과 질의 비용의 트레이드오프를 어떻게 판단하겠는가.
알고리즘에서 같이 읽기
- 이분 탐색 (Binary Search)정렬된 배열에서 탐색 범위를 절반씩 줄여 O(log n)에 값을 찾는 이분 탐색의 원리와 동작 과정을 그림으로 정리했다. lower bound와 upper bound, 코딩테스트 단골인 파라메트릭 서치와 회전 정렬 배열, 구현 코드와 자주 하는 실수까지 다룬다.
- 비트마스크 (Bitmask)정수의 각 비트로 집합의 포함 여부를 표현하는 비트마스크 기법을 다룬다. 기본 비트 연산 정리, 부분집합 열거, 외판원 순회 동적 계획법 구현과 주의점을 정리한다.
- DFS와 BFS (Depth First Search, Breadth First Search)그래프의 모든 정점을 방문하는 DFS와 BFS의 원리와 방문 순서 차이를 스택과 큐로 설명하고, O(V + E) 복잡도, 구현 코드, 문제 유형별 선택 기준을 정리한다.
- 다익스트라 알고리즘 (Dijkstra's Algorithm)음수 간선이 없는 그래프에서 한 정점으로부터의 최단 거리를 구하는 다익스트라 알고리즘의 그리디 원리, 우선순위 큐 구현, 복잡도 비교와 한계를 정리한다.
이 개념, 내 코드에서는 어떻게 물어볼까요
데브핏(DevFit)에 채용공고 URL을 붙여 넣고 레포지토리를 연결하면, 내가 쓴 커밋에서 예상 질문과 어필 포인트를 만듭니다. 하루 10회까지 무료입니다. 합불 판정이나 지원자 랭킹은 만들지 않고, 최종 판단은 사람이 합니다.
공고 넣고 시작하기