최소 공통 조상 알고리즘

참고 원본 - 최소 공통 조상 알고리즘 10분 정복

BOJ 11437 LCA 문제

BOJ 11438 LCA 2 문제

Table of Contents


최소 공통 조상 문제

  • Lowest Common Ancestor의 줄임말로 LCA문제라고도 한다.
  • Tree 구조에서 입력으로 들어오는 두 Node에 대해, 가장 가까운 공통 조상이 몇 번인지 찾아서 출력하는 문제이다.
  • 간단하게 생각해보면, 한 칸씩 올라가면서 같은지 다른지 확인하는 과정을 통해서 비교하면 될 것 같다고 생각되는데… 실제로 알고리즘도 그렇다.

기본 최소 공통 조상 알고리즘

  1. 우선 모든 Node의 깊이를 계산 (DFS 이용)
  2. 입력으로 들어온 2개의 Node를 확인하여, 더 깊은 Node의 깊이가 더 얕은 Node의 깊이와 같아지도록 올라가서, 두 Node의 깊이가 같도록 맞추기
  3. 두 Node의 깊이가 맞춰졌으면, 하나씩 올라가며, 부모가 같아지면 종료입니다.
import sys
input = sys.stdin.readline
sys.setrecursionlimit(int(1e5)) # 재귀함수의 호출 가능 횟수를 늘리는 부분
N = int(input())

parent = [0] * (N + 1) # 각 Node의 부모 정보를 담는 리스트
d = [0] * (N + 1) # 각 Node의 깊이
c = [0] * (N + 1) # 각 Node의 깊이가 계산 되었는 지 여부 (DFS 방문 여부 저장용)
graph = [[] for _ in range(N + 1)]

for _ in range(N - 1): # Tree의 Edge 개수는 N - 1이므로
    a, b = map(int, input().split())
    graph[a].append(b)
    graph[b].append(a)

def dfs(x, depth): # 깊이 계산용 dfs
    c[x] = True
    d[x] = depth
    for connected in graph[x]:
        if c[connected] == True:
            continue
        parent[connected] = x # 이미 조사를 했다는 얘기는 부모라는 의미이므로 남아있으면 자식
        dfs(connected, depth + 1) # 남아있는 자식은 depth + 1해서 호출

def lca(a, b): # a와 b의 공통 조상을 찾는 알고리즘
    if d[b] > d[a]: # a가 더 깊거나 같게 변경
        a, b = b, a
    
    while d[a] != d[b]: # a와 b의 깊이가 같을 때까지 반복
        a = parent[a] # a를 거슬러 올라가기
    
    while a != b: # 두 Node의 부모가 같을 때까지 반복
        a = parent[a] # a 거슬러 올라가기
        b = parent[b] # b 거슬러 올라가기
    
    return a

dfs(1, 0) # Root Node는 1번이며, 이는 깊이가 0

M = int(input()) # 입력할 개수를 M에 받을 예정

for i in range(M): # M개를 받아서, lca 함수 호출
    a, b = map(int, input().split())
    print(lca(a, b))
  • 위 내용을 살펴보면 최악의 경우는 $O(N)$의 시간 복잡도가 되며, $M$번을 처리해야한다면 $O(MN)$이 됩니다.
  • 이는 Tree가 한쪽으로 몰려있는 경우에 $O(N)$이 됩니다.
  • 근데 이상하게, Python3로 제출하면 시간 초과가 뜬다… 왜지…?
  • 더 시간 복잡도를 줄일 수 있는 방법이 있나…. 싶지만, PyPy3로는 성공했다.

심화 최소 공통 조상 알고리즘

  • 위와 같은 기본형태의 문제는 직관적이고 단순하지만, 일일히 다 살펴보는 과정이 필요하기 때문에, 시간 복잡도가 꽤 높은 편에 속한다.
  • 이와 같은 문제를 해결하기 위해서, 하나 하나 단계를 올라가는 방법이 아닌 $2^n$단계부터 $2^{n-1}$단계 … $2^{0}$단계까지 확인하는 방법을 사용할 수 있다.
  • 이렇게 올라갈 경우, $O(logN)의 시간 복잡도를 보장할 수 있다.
  • 단순하게 모든 숫자는 2와 1만 있으면 이에 대한 합으로 모두 표현할 수 있기 때문에 가능한 방법
  • ex) 15칸 거슬러 올라가기 $\rightarrow$ 8 + 4 + 2 + 1 단계 올라가기
  • ex) 13칸 거슬러 올라가기 $\rightarrow$ 8 + 4 + 1 단계 올라가기
  • 이와 같은 방법을 사용하기 위해 메모리를 더 사용하여, 각 Node의 $2^i$번째 부모에 대한 정보를 기록합니다.
  • 따라서, 공간 복잡도는 더 높아지지만, 시간 복잡도는 더 낮아지는 효과가 있음 (Trade-off)
import sys
input = sys.stdin.readline
sys.setrecursionlimit(int(1e5))
LOG = 21 # 2^20 ~~ 1,000,000 # 최대 입력 크기가 약 100만 정도이므로

N = int(input())
parent = [[0] * LOG for _ in range(N + 1)] # 모든 Node에 대해 최대 21개까지의 부모를 저장할 수 있는 공간 할당
d = [0] * (N + 1) # 깊이 저장 공간
c = [0] * (N + 1) # 깊이 계산 여부 저장 공간
graph = [[] for _ in range(N + 1)] # Graph 정보 저장

for _ in range(N - 1): # Tree에서 N개의 Node에 대해 필요한 값은 N - 1개의 Edge
    a, b = map(int, input().split())
    graph[a].append(b)
    graph[b].append(a)

def dfs(x, depth):
    c[x] = True
    d[x] = depth
    for connected in graph[x]:
        if c[connected] == True:
            continue
        parent[connected][0] = x
        dfs(connected, depth + 1)

def set_parent():
    dfs(1, 0) # 1번이 Root Node이며, Depth가 0
    for i in range(1, LOG): # 최대 LOG개수까지 진행
        for j in range(1, N + 1): # 모든 Node에 대해 진행
            parent[j][i] = parent[parent[j][i - 1]][i - 1] # 2 ^ i의 부모는 2 ^ (i - 1) 부모의 2 ^ (i - 1)부모를 의미

def lca(a, b):
    if d[b] > d[a]: # 무조건 a가 더 깊거나 같게 설정
        a, b = b, a
    
    for i in range(LOG - 1, -1, -1): # 높은 수부터 확인해서
        if d[a] - d[b] >= 2 ** i: # 깊이 차이가 더 커지면
            a = parent[a][i] # 올라가기
    
    if a == b: # 혹시라도 두 값이 같아서, 이미 공통조상이면 반환
        return a
    
    for i in range(LOG - 1, -1, -1): # 높은 수부터 확인해서
        if parent[a][i] != parent[b][i]: # 두 수의 부모가 다르면
            a = parent[a][i] # 거슬러 올라가기
            b = parent[b][i] # 거슬러 올라가기
    
    return parent[a][0] # 다를때만 올라갔으므로, 하나 더 올라가야 공통 조상임

set_parent() # 먼저 부모를 설정

M = int(input()) # 계산할 횟수를 M에 저장

for _ in range(M):
    a, b = map(int, input().split())
    print(lca(a, b)) # 입력받은 값의 공통 조상을 찾아서 출력
  • 이와 같이 개선하면 M번 처리하는데 $O(MlogN)$이면 찾을 수 있습니다.
  • 이 문제도 Python3로 제출하면… 시간초과가 뜬다… 꼭 PyPy3로 제출해야한다.

Review

  • 특별한 내용은 없었지만, 2의 제곱수의 조합을 이용하여 여러 단계를 한 번에 올라가는 방법을 사용하는 것이 꽤 신기했다.
  • 그나저나 왜 시간 초과가 뜨는거지…ㅠㅠ 아직 배워야할 것이 많은가보다…

comments