Home [백준 11438번] LCA 2 [C++]
Post
Cancel

[백준 11438번] LCA 2 [C++]

11438번: LCA 2

문제 링크: 11438번: LCA 2


개요

최소 공통 조상(Lowest common ancestor, LCA)는 트리에 어떤 두 정점 u와 v가 있을 때, u 자신이거나 u의 조상인 동시에 v 자신이거나 v의 조상인 노드들 중 가장 깊은 노드이다. LCA 알고리즘의 기본 흐름은 다음과 같다.

  1. 두 정점 중 깊이가 더 깊은 정점에서 출발한다.
  2. 다른 정점의 깊이와 같아질 때까지 부모 노드로 이동한다.
  3. 깊이가 같은 두 정점에서 동시에 출발하여 부모 노드로 이동한다.
  4. 한 노드에서 만났을 때 이동을 종료한다.
  5. 만난 노드가 LCA이며, 총 이동 횟수가 두 노드 간의 최소 거리이다.

전처리 (depth 배열과 parent 배열)

1
2
3
vector<int> tree[MAX_N + 1];
int parent[MAX_N + 1][MAX_LOG_N]; //parent[n][k] : 노드 n의 2^k번째 부모
int depth[MAX_N + 1] = { 0 };

tree[i]는 노드 i와 연결된 모든 노드의 번호를 저장하는 vector이며, depth[i]은 노드 i의 깊이를 저장한다. 노드 1은 루트 노드이기 때문에 depth[1]은 0이다.
i번째 노드의 1번째 부모가 parent[i]인 일차원 배열을 사용해도 위에서 말한 흐름을 따라가며 LCA를 찾을 수 있다. 그러나 해당 방식은 부모 노드로 타고 올라가는 과정의 시간 복잡도가 O(n)이기 때문에 전체 시간 복잡도가 O(n*m)이 되어 이 문제에서는 시간 초과가 발생하게 된다. 따라서 노드 n의 2^k번째 부모(1번째 부모가 바로 위의 노드)를 parent[n][k]에 저장하는 이차원 배열을 사용했다.

parent[n][0]와 depth 배열 채우기

1
2
3
4
5
6
7
8
9
10
11
12
void explore(int currNode) {
	for (int i = 0; i < tree[currNode].size(); i++) {
		int nextNode = tree[currNode][i];
		if (depth[nextNode] == -1) {
			parent[nextNode][0] = currNode;
			depth[nextNode] = depth[currNode] + 1;

			explore(nextNode);
		}
	}
	return;
}

일반적인 DFS의 형태를 가진 재귀 함수이다. 메인 함수에서 explore(1)을 호출하여 루트 노드인 1에서부터 출발해 자식 노드로 내려가며 각 노드의 depth와 parent[n][0] (노드 n의 첫 번째 부모) 에 값을 넣어준다.

parent 배열 나머지 채우기

1
2
3
4
5
6
7
for (int j = 0; j < MAX_LOG_N; j++) {
    for (int i = 2; i <= n; i++) {
        if (parent[i][j] != -1) {
            parent[i][j + 1] = parent[parent[i][j]][j];
        }
    }
}

explore() 함수가 실행되면서 parent[n][0]만 채워졌다. 노드 n의 2^k번째 부모를 parent[n][k]에 넣어줘야 한다. 어떤 노드 i의 j번째 조상의 j번째 조상은 i의 2*j번째 조상이고, 이는 곧 parent[i][j + 1]이다. 따라서 parent[i][j + 1]는 parent[parent[i][j]][j]의 형태로 나타낼 수 있다.


쿼리 처리하기

depth[u] >= depth[v]로 유지

1
if (depth[u] < depth[v]) { int tmp = u; u = v, v = tmp; };

이후 작업을 편하게 하기 위해 u의 깊이가 v의 깊이보다 깊거나 같게 되도록 조건문을 추가했다.

두 노드의 깊이 맞추기

1
2
3
4
5
int diff = depth[u] - depth[v];
for (int j = 0; diff > 0; j++) {
    if (diff % 2 == 1) u = parent[u][j];
    diff /= 2;
}

두 노드의 깊이 차이를 diff에 저장해놓고, u의 깊이가 v의 깊이와 같아질 때까지 parent[u]를 이용해 u를 부모 노드로 이동시키는 작업이다. j는 밑이 2인 로그 스케일로 변화하는 변수이기 때문에 2진수 변환과 같은 방법을 이용했다. 부모 노드로 한 칸씩 이동하는 방법보다 훨씬 빠르게 두 노드의 깊이를 맞출 수 있다.

LCS 구하기

1
2
3
4
5
6
7
8
9
10
11
if (u != v) {
    for (int j = MAX_LOG_N - 1; j >= 0; j--) {
        if (parent[u][j] != -1 && parent[u][j] != parent[v][j]) {
            u = parent[u][j];
            v = parent[v][j];
        }
    }
    u = parent[u][0];
}

printf("%d\n", u);

두 노드의 깊이가 같아졌으므로 부모 노드가 다른 동안 부모 노드로 한 칸씩 이동해준다. 이동 후 u와 v는 LCS의 바로 자식 노드이기 때문에 마지막으로 한 칸 더 이동 후 출력해준다.


전체 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
#include <iostream> 
#include <vector> 
#pragma warning(disable:4996)
#pragma warning(disable:6031)
using namespace std;

#define MAX_N 100000
#define MAX_LOG_N 18

vector<int> tree[MAX_N + 1];
int parent[MAX_N + 1][MAX_LOG_N]; //parent[n][k] : n번째 노드의 2^k번째 부모
int depth[MAX_N + 1] = { 0 };

void explore(int);

int main() {
	int n, m;
	scanf("%d", &n);

	//트리 입력받기
	for (int i = 0; i < n - 1; i++) {
		int a, b;
		cin >> a >> b;
		tree[a].push_back(b);
		tree[b].push_back(a);
	}

	//depth, parent 배열 초기화
	for (int i = 1; i <= n; i++) {
		depth[i] = -1;
		for (int j = 0; j < MAX_LOG_N; j++) {
			parent[i][j] = -1;
		}
	}
	depth[1] = 0;

	//parent[n][0]과 depth 배열 채우기
	explore(1);

	//parent 배열 채우기
	for (int j = 0; j < MAX_LOG_N; j++) {
		for (int i = 2; i <= n; i++) {
			if (parent[i][j] != -1) {
				parent[i][j + 1] = parent[parent[i][j]][j];
			}
		}
	}

	//쿼리 입력받기
	scanf("%d", &m);
	while (m--) {
		int u, v;
		scanf("%d %d", &u, &v);

		//depth[u] >= depth[v]로 유지
		if (depth[u] < depth[v]) { int tmp = u; u = v, v = tmp; };
		int diff = depth[u] - depth[v];
		
		//두 노드의 깊이 맞추기
		for (int j = 0; diff > 0; j++) {
			if (diff % 2 == 1) u = parent[u][j];
			diff /= 2;
		}

		if (u != v) {
			for (int j = MAX_LOG_N - 1; j >= 0; j--) {
				if (parent[u][j] != -1 && parent[u][j] != parent[v][j]) {
					u = parent[u][j];
					v = parent[v][j];
				}
			}
			u = parent[u][0];
		}

		printf("%d\n", u);
	}


	return 0;
}

void explore(int currNode) {
	for (int i = 0; i < tree[currNode].size(); i++) {
		int nextNode = tree[currNode][i];
		if (depth[nextNode] == -1) {
			parent[nextNode][0] = currNode;
			depth[nextNode] = depth[currNode] + 1;

			explore(nextNode);
		}
	}
	return;
}

This post is licensed under CC BY 4.0 by the author.

[Unity] 객체 지향과 유니티 기초 개념

[백준 1774번] 우주신과의 교감 [C++]