최소 공통 조상 (LCA, Lowest Common Ancestor)

한눈에 보기

트리에서 두 노드의 가장 깊은 공통 조상을 찾는 LCA 알고리즘을 다룬다. 단순 상승 방법부터 희소 테이블 기반 이진 리프팅까지 원리, 질의 과정, 구현 코드를 정리한다.

최종 업데이트 2026.09.17

최소 공통 조상(LCA, Lowest Common Ancestor)은 트리에서 두 노드의 공통 조상 중 가장 깊은 노드를 말한다. 두 노드가 갈라지기 직전의 갈림길이라고 생각하면 쉽다.

트리에서 두 노드 사이의 거리를 구하거나, 경로 위의 값을 다루는 문제가 대부분 LCA로 환원된다. 질의가 한두 번이면 단순하게 올라가도 되지만, 질의가 10만 번씩 들어오면 희소 배열로 O(log n)에 답해야 한다. 이 글은 그 두 단계를 순서대로 다룬다.

무엇을 구하는 문제인가

최소 공통 조상 LCA: 트리에서 노드 8과 노드 5의 최소 공통 조상이 노드 2임을 보여주는 그림
LCA(8, 5) = 2. 8은 4를 거쳐, 5는 곧바로 2에 도달한다. 1도 공통 조상이지만 2가 더 깊으므로 답은 2다. 두 노드 사이의 거리도 여기서 나온다 — 깊이(8) + 깊이(5) − 2 × 깊이(2) = 3 + 2 − 2 = 3.

가장 단순한 방법

두 노드의 깊이가 다르면 먼저 깊은 쪽을 끌어올려 깊이를 맞춘다. 그다음 둘이 같아질 때까지 한 칸씩 함께 올라간다. 만나는 지점이 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칸 위로 두 번 가는 것과 같다는 뜻이다. 이미 계산해둔 절반짜리 점프를 두 번 이어 붙여 두 배짜리 점프를 만든다.

최소 공통 조상 LCA: 한 갈래 경로에서 2의 0제곱, 1제곱 크기의 점프로 조상을 건너뛰는 그림
13칸 위로 올라가야 한다면 13을 이진수 1101₂로 보고 2³, 2², 2⁰ 점프를 차례로 하면 된다. 어떤 거리든 최대 log n 번의 점프로 끝나는 이유다.

LCA를 구할 때도 같은 요령을 두 번 쓴다.

  1. 깊이 맞추기: 깊이 차이를 이진수로 쪼개, 해당하는 비트마다 점프한다.
  2. 갈라지는 지점 찾기: 큰 점프부터 시도하며 두 노드의 조상이 서로 다를 때만 올라간다. 조상이 같아지는 점프는 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로 어떻게 구하는가.
  • 전처리 비용과 질의 비용의 트레이드오프를 어떻게 판단하겠는가.
알고리즘 문서 전체 보기

이 개념, 내 코드에서는 어떻게 물어볼까요

데브핏(DevFit)에 채용공고 URL을 붙여 넣고 레포지토리를 연결하면, 내가 쓴 커밋에서 예상 질문과 어필 포인트를 만듭니다. 하루 10회까지 무료입니다. 합불 판정이나 지원자 랭킹은 만들지 않고, 최종 판단은 사람이 합니다.

공고 넣고 시작하기