최소 공통 조상 알고리즘
Table of Contents
최소 공통 조상 문제
- Lowest Common Ancestor의 줄임말로
LCA
문제라고도 한다.
Tree
구조에서 입력으로 들어오는 두 Node에 대해, 가장 가까운 공통 조상이 몇 번인지 찾아서 출력하는 문제이다.
- 간단하게 생각해보면, 한 칸씩 올라가면서 같은지 다른지 확인하는 과정을 통해서 비교하면 될 것 같다고 생각되는데… 실제로 알고리즘도 그렇다.
기본 최소 공통 조상 알고리즘
- 우선 모든 Node의 깊이를 계산 (
DFS
이용)
- 입력으로 들어온 2개의 Node를 확인하여, 더 깊은 Node의 깊이가 더 얕은 Node의 깊이와 같아지도록 올라가서, 두 Node의 깊이가 같도록 맞추기
- 두 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의 제곱수의 조합을 이용하여 여러 단계를 한 번에 올라가는 방법을 사용하는 것이 꽤 신기했다.
- 그나저나 왜 시간 초과가 뜨는거지…ㅠㅠ 아직 배워야할 것이 많은가보다…