기본편
개념
- 정의: 두 노드의 공통된 조상 중 가장 가까운 조상을 찾는 문제
- 동작 과정:
- 모든 노드에 대한 깊이(depth) 계산
- 최소 공통 조상을 찾을 두 노드를 확인 - LCA(a,b) 연산에 대하여 아래 과정을 반복
- 먼저 두 노드의 깊이(depth)가 동일하도록 거슬러 올라감
- 이후 부모가 같아질 때까지 반복적으로 두 노드의 부모 방향으로 거슬러 올라감
- 시간 복잡도: O(N) - 매 쿼리마다 부모 방향으로 거슬러 올라가기 때문에
- 언제 사용?:
- m번의 LCA연산으로 총 O(MN) 시간 복잡도가 괜찮을 경우
- 메모리 여유가 없을 경우
구현
//n: 노드 개수, m: LCA연산 횟수
static int n, m;
static int[] parent = new int[n+1]; // 부모 노드에 대한 정보
static int[] depth = new int[n+1]; // 각 노드까지의 깊이
static boolean[] checked = new boolean[n+1]; // 깊이 계산 여부
static ArrayList<Integer>[] graph = new ArrayList[n+1]; // 그래프 정보
1. 모든 노드 깊이(depth) 계산
DFS를 이용해 모든 노드에 대하여 깊이(depth)를 계산
코드
// 깊이(depth)를 구하는 함수:
// x - 노드 번호
// d - 깊이
public static void dfs(int x, int d) {
//자신(x)의 깊이를 d로 지정
checked[x] = true;
depth[x] = d;
//자신(x)의 자식 노드(y)에 대해서 처리 진행(재귀)
for (int y : graph[x]) {
if (checked[y]) continue; // 이미 깊이를 구했다면 넘어가기
parent[y] = x;
dfs(y, d + 1);
}
}
2. LCA(A, B) 연산 진행
먼저 두 노드의 깊이를 맞춤
부모가 같아질 땔까지 거슬러 올라감
코드
// 최소 공통 조상을 찾는 함수
public static int lca(int a, int b) {
// 먼저 깊이가 동일하도록 조정
while (depth[a] != depth[b]) {
if (depth[a] > depth[b]) a = parent[a];
else b = parent[b];
}
// 노드가 같아질 때까지 올라가기
while (a != b) {
a = parent[a];
b = parent[b];
}
return a;
}
최종 코드
import java.util.ArrayList;
import java.util.Scanner;
public class Main {
static int n, m;
static int[] parent; // 부모 노드에 대한 정보
static int[] depth; // 각 노드까지의 깊이
static boolean[] checked; // 깊이 계산 여부
static ArrayList<Integer>[] graph; // 그래프 정보
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
n = scanner.nextInt();
//각 테이블 정보 초기화
parent = new int[n+1];
depth = new int[n+1];
checked = new int[n+1];
// 그래프 초기화
graph = new ArrayList[n+1];
for (int i = 0; i < MAX; i++) {
graph[i] = new ArrayList<>();
}
//그래프 정보 입력 받기
for (int i = 0; i < n - 1; i++) {
int a = scanner.nextInt();
int b = scanner.nextInt();
graph[a].add(b);
graph[b].add(a);
}
//1. 루트 노드부터 깊이 계산 시작
dfs(1, 0);
//2. LCA 연산 진행
m = scanner.nextInt();
for (int i = 0; i < m; i++) {
int a = scanner.nextInt();
int b = scanner.nextInt();
System.out.println(lca(a, b));
}
scanner.close();
}
// 깊이를 구하는 함수
public static void dfs(int x, int d) {
checked[x] = true;
depth[x] = d;
for (int y : graph[x]) {
if (checked[y]) continue; // 이미 깊이를 계산했다면 넘어가기
parent[y] = x;
dfs(y, d + 1);
}
}
// 최소 공통 조상을 찾는 함수
public static int lca(int a, int b) {
// 먼저 깊이가 동일하도록 조정
while (depth[a] != depth[b]) {
if (depth[a] > depth[b]) a = parent[a];
else b = parent[b];
}
// 노드가 같아질 때까지 올라가기
while (a != b) {
a = parent[a];
b = parent[b];
}
return a;
}
}
개선편
개념
- 시간복잡도: O(N) -> O(logN)으로 개선
- 방법: 각 노드가 거슬러 올라가는 속도를 빠르게 진행
- = 2의 제곱 형태로 거슬러 올라감
- 즉, 모든 노드에 대하여 2^i 번째 부모에 대한 정보를 기록
- 1차원 부모 테이블(parent[k]) -> 2차원 부모 테이블(parent[k][i])로 확장
구현
//n: 노드 개수, m: LCA연산 횟수
static int n, m;
//@@ 변경: 1차원 부모 테이블 -> 2차원 부모 테이블
static int[][] parent = new int[n+1][n+1]; // 부모 노드에 대한 정보
static int[] depth = new int[n+1]; // 각 노드까지의 깊이
static boolean[] checked = new boolean[n+1]; // 깊이 계산 여부
static ArrayList<Integer>[] graph = new ArrayList[n+1]; // 그래프 정보
1. 모든 노드 깊이(depth) 계산
DFS를 이용해 모든 노드에 대하여 깊이(depth)를 계산
코드
// 깊이(depth)를 구하는 함수
public static void dfs(int x, int d) {
checked[x] = true;
depth[x] = d;
for (int y : graph[x]) {
if (checked[y]) continue; // 이미 깊이를 구했다면 넘어가기
parent[y][0] = x; //@@변경: parent[y] -> parent[y][0]
dfs(y, d + 1);
}
}
//@@추가: 전체 부모 관계를 설정하는 함수
public static void setParent() {
dfs(1, 0); // 루트 노드는 1번 노드로 시작
//2^i 앞에 있는 부모 정보 기록: 1,2,4,8....
for (int i = 1; i < LOG; i++) {
for (int j = 1; j <= n; j++) {
parent[j][i] = parent[parent[j][i - 1]][i - 1];
}
}
}
2. LCA(A, B) 연산 진행
먼저 두 노드의 깊이를 맞춤
부모가 같아질 땔까지 거슬러 올라감
이때 2의 제곱꼴로 올라감
코드
// 최소 공통 조상(LCA)을 찾는 함수
public static int lca(int a, int b) {
// 더 깊은 노드를 b로 설정
if (depth[a] > depth[b]) {
int temp = a;
a = b;
b = temp;
}
// 먼저 깊이가 동일하도록 조정
for (int i = LOG - 1; i >= 0; i--) {
if (depth[b] - depth[a] >= (1 << i)) {
b = parent[b][i];
}
}
// 부모가 같아질 때까지
if (a == b) return a;
for (int i = LOG - 1; i >= 0; i--) {
if (parent[a][i] != parent[b][i]) {
a = parent[a][i];
b = parent[b][i];
}
}
return parent[a][0];
}
최종 코드
import java.util.ArrayList;
import java.util.Scanner;
public class Main {
static final int MAX = 100001;
static final int LOG = 21; // 2^20 = 1,000,000
static int n, m;
static int[][] parent = new int[MAX][LOG]; // 부모에 대한 정보
static int[] depth = new int[MAX]; // 각 노드까지의 깊이
static boolean[] checked = new boolean[MAX]; // 각 노드의 깊이가 계산되었는지 여부
static ArrayList<Integer>[] graph = new ArrayList[MAX]; // 그래프 정보
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
n = scanner.nextInt();
for (int i = 0; i < MAX; i++) {
graph[i] = new ArrayList<>();
}
for (int i = 0; i < n - 1; i++) {
int a = scanner.nextInt();
int b = scanner.nextInt();
graph[a].add(b);
graph[b].add(a);
}
setParent();
m = scanner.nextInt();
for (int i = 0; i < m; i++) {
int a = scanner.nextInt();
int b = scanner.nextInt();
System.out.println(lca(a, b));
}
scanner.close();
}
// 깊이(depth)를 구하는 함수
public static void dfs(int x, int d) {
checked[x] = true;
depth[x] = d;
for (int y : graph[x]) {
if (checked[y]) continue; // 이미 깊이를 구했다면 넘어가기
parent[y][0] = x;
dfs(y, d + 1);
}
}
// 전체 부모 관계를 설정하는 함수
public static void setParent() {
dfs(1, 0); // 루트 노드는 1번 노드로 시작
for (int i = 1; i < LOG; i++) {
for (int j = 1; j <= n; j++) {
parent[j][i] = parent[parent[j][i - 1]][i - 1];
}
}
}
// 최소 공통 조상(LCA)을 찾는 함수
public static int lca(int a, int b) {
// 더 깊은 노드를 b로 설정
if (depth[a] > depth[b]) {
int temp = a;
a = b;
b = temp;
}
// 먼저 깊이가 동일하도록 조정
for (int i = LOG - 1; i >= 0; i--) {
if (depth[b] - depth[a] >= (1 << i)) {
b = parent[b][i];
}
}
// 부모가 같아질 때까지
if (a == b) return a;
for (int i = LOG - 1; i >= 0; i--) {
if (parent[a][i] != parent[b][i]) {
a = parent[a][i];
b = parent[b][i];
}
}
return parent[a][0];
}
}
'코딩테스트 > 알고리즘' 카테고리의 다른 글
그래프 탐색 유형 오답노트 (0) | 2024.08.21 |
---|---|
14. 그래프 - 트리(Tree), 이진 탐색 트리(BST), 트리 순회(Tree Traversal) (2) | 2024.04.02 |
12. 그래프 - 위상 정렬 (0) | 2024.04.02 |
11. 그래프 - 무방향 그래프 사이클 판별, 최소 신장 트리 (0) | 2024.04.02 |
10-2. 그래프 - 음수 간선의 최단 거리 - 벨만 포드 알고리즘 (0) | 2024.04.02 |