코틀린 코딩테스트 강좌, 최소 신장 트리 구하기

작성일: 2023년 10월 21일

소개

프로그래밍 시험에서 자주 등장하는 최소 신장 트리(Minimum Spanning Tree, MST) 문제는 주로 그래프 이론의 중요한 개념 중 하나입니다. 최소 신장 트리는 그래프의 모든 정점을 포함하면서 가중치의 합을 최소화하는 트리 구조를 의미합니다.
이 글에서는 Kotlin 언어를 사용하여 최소 신장 트리를 구하는 알고리즘을 자세히 살펴보고, 관련 문제 풀이 예제를 통해 이론을 적용하는 방법을 배워보겠습니다.

기본 개념

그래프

그래프는 정점(vertices)과 간선(edges)으로 구성된 데이터 구조입니다. 정점은 노드, 간선은 노드 사이의 연결을 나타냅니다.
그래프는 방향 그래프와 무방향 그래프로 구분할 수 있으며, 간선에 가중치가 존재하면 가중치 그래프로 불립니다.

최소 신장 트리(MST)

최소 신장 트리는 가중치 그래프에서 모든 정점을 연결하는 서브그래프 중에서 가중치의 합이 최소인 트리입니다.
MST를 찾는 알고리즘으로는 Prim의 알고리즘과 Kruskal의 알고리즘이 널리 사용됩니다.

문제 설명

문제 정의

당신은 무방향 그래프와 그 간선의 가중치가 주어졌습니다. 이 그래프의 모든 정점을 포함하는 최소 신장 트리를 찾아 그 가중치의 합을 출력하는 프로그램을 작성하세요.

입력 형식

  • 첫 번째 줄에는 정점의 수 V와 간선의 수 E가 주어집니다. (1 ≤ V ≤ 1000, 1 ≤ E ≤ 10000)
  • 다음 E줄에는 간선의 두 정점과 가중치가 주어집니다.

출력 형식

최소 신장 트리의 가중치의 합을 출력합니다.

문제 풀이 과정

Kruskal의 알고리즘을 이용한 접근

Kruskal의 알고리즘은 간선을 가중치 기준으로 정렬한 후, 사이클을 형성하지 않는 간선을 선택하여 최소 신장 트리를 구성하는 방식입니다.
이 알고리즘은 주어진 그래프의 간선을 정렬하고, 유니온-파인드 데이터 구조를 이용하여 사이클 여부를 확인합니다.
이 과정은 다음과 같은 단계로 진행됩니다.

  1. 간선 정렬: 모든 간선을 가중치 기준으로 오름차순 정렬합니다.
  2. 유니온-파인드 초기화: 각 정점이 독립된 집합으로 초기화합니다.
  3. 최소 신장 트리 구축: 정렬된 간선을 순회하며 사이클이 없으면 해당 간선을 MST에 추가합니다.

코틀린 구현

코드 예제

class Edge(val u: Int, val v: Int, val weight: Int): Comparable {
    override fun compareTo(other: Edge) = this.weight - other.weight
}

class DisjointSet(val size: Int) {
    private val parent = IntArray(size) { it }
    private val rank = IntArray(size) { 0 }

    fun find(x: Int): Int {
        if (parent[x] != x) {
            parent[x] = find(parent[x])
        }
        return parent[x]
    }

    fun union(x: Int, y: Int): Boolean {
        val rootX = find(x)
        val rootY = find(y)

        if (rootX == rootY) return false

        if (rank[rootX] > rank[rootY]) {
            parent[rootY] = rootX
        } else if (rank[rootX] < rank[rootY]) {
            parent[rootX] = rootY
        } else {
            parent[rootY] = rootX
            rank[rootX]++
        }
        return true
    }
}

fun kruskal(vertices: Int, edges: List): Int {
    val disjointSet = DisjointSet(vertices)
    val sortedEdges = edges.sorted()
    
    var totalWeight = 0
    
    for (edge in sortedEdges) {
        if (disjointSet.union(edge.u, edge.v)) {
            totalWeight += edge.weight
        }
    }

    return totalWeight
}

fun main() {
    val reader = System.`in`.bufferedReader()
    val (V, E) = reader.readLine().split(" ").map { it.toInt() }
    val edges = mutableListOf()

    for (i in 0 until E) {
        val (u, v, weight) = reader.readLine().split(" ").map { it.toInt() }
        edges.add(Edge(u - 1, v - 1, weight))
    }

    val result = kruskal(V, edges)
    println(result)
}

코드 설명

위 코드는 Kruskal의 알고리즘을 기반으로 최소 신장 트리를 찾는 과정입니다.
Edge 클래스는 간선 정보를 저장하며, DisjointSet 클래스는 유니온-파인드 알고리즘에 기반하여 집합의 대표자를 찾고, 두 집합을 합치는 기능을 제공 합니다.

kruskal 함수에서는 입력 받은 정점 수와 간선 목록을 사용하여 모든 간선을 가중치 기준으로 정렬합니다.
그런 다음, 정렬된 간선을 순회하며 사이클이 발생하지 않는 경우에만 해당 간선을 MST에 추가합니다.
마지막으로 MST의 총 가중치를 반환합니다.

테스트 케이스

예제 입력

5 6
1 2 3
1 3 4
2 3 1
2 4 6
3 4 5
4 5 2

예제 출력

10

설명

주어진 그래프는 5개의 정점과 6개의 간선으로 구성되어 있으며, 이때 최소 신장 트리는 가중치 1, 2, 3, 4의 간선으로 이루어져 총 가중치 10을 갖습니다.

결론

이번 글에서는 코틀린을 사용하여 최소 신장 트리를 찾는 알고리즘을 구현하고, 그 과정에 대한 이해를 높이기 위해 문제를 풀어보았습니다.
최소 신장 트리는 다양한 실제 문제에 적용될 수 있는 중요한 개념이며, Kruskal의 알고리즘 외에도 Prim의 알고리즘과 같은 다른 접근법도 존재합니다.
이러한 알고리즘을 활용하면 효율적으로 그래프 문제를 해결할 수 있습니다.
앞으로도 여러 가지 알고리즘을 소개하여 코딩 테스트에서의 성공적인 결과를 거둘 수 있도록 도와드리겠습니다.

코틀린 코딩테스트 강좌, 최소 신장 트리

1. 최소 신장 트리란?

최소 신장 트리(Minimum Spanning Tree, MST)는 가중치가 부여된 그래프에서 모든 정점을 포함하면서 가중치의 합이 최소가 되도록 하는 부분 그래프를 말합니다. 즉, 트리의 모든 정점이 연결되어 있으며 주어진 간선의 가중치의 합이 가장 작은 트리를 의미합니다. 최소 신장 트리는 네트워크 설계, 도로 건설, 통신망 구축 등 다양한 분야에서 활용됩니다.

2. 알고리즘 문제 제시

다음은 최소 신장 트리를 구하는 문제입니다:

문제 설명

주어진 정점 수 V와 간선 수 E가 있는 그래프가 주어질 때, 최소 신장 트리의 가중치의 합을 구하는 프로그램을 코틀린으로 작성하세요. 간선은 가중치와 함께 주어집니다.

입력 형식

  • 첫 번째 줄에 정점 수 V와 간선 수 E가 주어진다.
  • 다음 E개의 줄에는 각 간선의 정보 u, v, w (정점 u와 v를 연결하는 간선의 가중치 w)가 주어진다.

출력 형식

최소 신장 트리의 가중치의 합을 출력한다.

3. 문제 해결 방법론

3.1. 알고리즘 선택

최소 신장 트리를 구하는 방법으로는 Prim 알고리즘과 Kruskal 알고리즘이 있습니다. 본 강좌에서는 Prim 알고리즘을 사용하여 문제를 해결하겠습니다. Prim 알고리즘은 다음과 같은 절차로 진행됩니다.

  1. 임의의 정점에서 시작하여 연결된 간선 중 가장 가중치가 작은 간선을 선택하여 트리에 포함시킵니다.
  2. 트리에 포함된 정점과 연결된 모든 간선 중에서 가중치가 가장 작은 간선을 선택합니다.
  3. 이 과정을 모든 정점이 트리에 포함될 때까지 반복합니다.

3.2. 시간 복잡도

Prim 알고리즘의 시간 복잡도는 O(E log V)입니다. 여기서 E는 간선의 수, V는 정점의 수입니다. 반면, Kruskal 알고리즘은 O(E log E)로 비슷하지만 서로 다른 경우에 더 효율적일 수 있습니다.

4. 코틀린 코드 구현

아래는 Prim 알고리즘을 사용한 최소 신장 트리의 가중치 합을 구하는 코틀린 코드입니다:


fun findMinimumSpanningTree(V: Int, edges: List>): Int {
    val graph = Array(V) { mutableListOf>() }
    for (edge in edges) {
        graph[edge.first].add(Pair(edge.second, edge.third))
        graph[edge.second].add(Pair(edge.first, edge.third))
    }

    val visited = BooleanArray(V)
    val minHeap = PriorityQueue>(compareBy { it.second })
    minHeap.add(Pair(0, 0)) // (정점, 가중치)
    var totalWeight = 0

    while (minHeap.isNotEmpty()) {
        val (u, weight) = minHeap.poll()
        if (visited[u]) continue
        visited[u] = true
        totalWeight += weight

        for ((v, w) in graph[u]) {
            if (!visited[v]) {
                minHeap.add(Pair(v, w))
            }
        }
    }

    return totalWeight
}
            

4.1. 메인 함수와 입력 부분

위의 `findMinimumSpanningTree` 함수를 사용할 메인 함수와 입력 처리 부분은 다음과 같습니다:


fun main() {
    val (V, E) = readLine()!!.split(" ").map { it.toInt() }
    val edges = mutableListOf>()

    repeat(E) {
        val edge = readLine()!!.split(" ").map { it.toInt() }
        edges.add(Triple(edge[0], edge[1], edge[2]))
    }

    val result = findMinimumSpanningTree(V, edges)
    println(result)
}
            

5. 문제 풀이 과정에서의 주의 사항

최소 신장 트리를 구하는 과정에서 몇 가지 주의해야 할 사항들이 있습니다:

  • 그래프의 정점은 인덱스 0부터 시작하는 경우가 많으므로 입력할 때 주의해야 합니다.
  • 모든 정점을 방문해야 하므로, visited 배열의 초기화를 잊지 않도록 합니다.
  • 모든 간선의 가중치가 같은 경우에도 알고리즘을 잘 수행할 수 있도록 설계해야 합니다.
  • 최종적으로 출력되는 가중치의 합이 올바른지 확인합니다.
  • 간선이 중복되지 않도록 처리해야 합니다.

6. 예제 테스트 케이스

다음은 최소 신장 트리 문제를 테스트하기 위한 예제 케이스입니다:

입력 예시

4 5
0 1 1
0 2 3
1 2 1
1 3 4
2 3 1

출력 예시

3

예제 입력은 4개의 정점과 5개의 간선으로 이루어진 그래프입니다. 최소 신장 트리를 구성하는 간선의 가중치의 합은 3이어야 합니다.

7. 결론

이번 강좌에서는 코틀린을 사용하여 최소 신장 트리를 구하는 Prim 알고리즘을 구현해보았습니다. 이 알고리즘은 효율적이며 구현하기도 쉬워 다양한 문제에 적용할 수 있습니다. 알고리즘 문제 풀이 과정에서 그래프의 구조를 이해하고 최적의 방식으로 문제를 해결하는 능력을 키우는 것이 중요합니다. 앞으로도 다양한 알고리즘 문제를 통해 실력을 향상시켜 나가길 바랍니다.

코틀린 코딩테스트 강좌, 최소 비용 구하기

안녕하세요! 이번 포스팅에서는 코틀린으로 해결할 수 있는 알고리즘 문제를 다루어 보겠습니다.
주제는 최소 비용 구하기입니다. 이 문제는 다양한 형태로 출제될 수 있으며, 코딩 테스트에서 자주 접하는 유형입니다.
이 글에서는 문제 설명, 알고리즘 설계, 코틀린 코드 구현, 예제 및 테스트 케이스를 포함한 체계적인 문제 해결 과정을 설명하겠습니다.

문제 설명

N개의 정점과 M개의 간선이 연결된 방향 그래프가 있습니다. 각 간선은 비가역적이며, 특정 비용이 부여되어 있습니다.
여러분의 임무는 특정 시작 정점에서 목표 정점까지의 최소 비용을 구하는 것입니다.
간선들은 다양하게 연결되어 있을 수 있으며, 같은 정점으로 가는 여려 경로가 있을 수 있습니다.

입력 형식:

  • 첫째 줄에 정점의 개수 N과 간선의 개수 M이 주어진다.
  • 둘째 줄부터 M줄에 걸쳐 간선 정보: 시작 정점 A, 도착 정점 B, 비용 C가 주어진다.
  • 마지막 줄에는 시작 정점과 목표 정점이 주어진다.

출력 형식:

  • 시작 정점에서 목표 정점까지 도달하는 최소 비용을 출력한다. 만약 도달할 수 없다면 -1을 출력한다.

예시 입력

5 6
1 2 2
1 3 3
2 4 1
3 4 4
2 5 5
4 5 1
1 5

예시 출력

8

문제 해결 접근법

이 문제는 최단 경로 문제로, Dijkstra 알고리즘을 사용하여 효과적으로 해결할 수 있습니다. Dijkstra 알고리즘은
그래프에서 특정 출발 정점으로부터 다른 모든 정점까지의 최단 경로를 구하는 데 탁월한 성능을 보입니다.
본 장에서는 Dijkstra 알고리즘의 개요와 코틀린으로 구현하는 방법에 대해 자세히 살펴보겠습니다.

Dijkstra 알고리즘 개요

Dijkstra 알고리즘은 다음과 같은 원리에 기반하여 작동합니다:

  • 시작 정점에서 다른 모든 정점까지의 최단 경로를 초기화합니다. 시작 정점으로부터의 거리는 0으로 설정하고, 나머지 정점은 무한대로 설정합니다.
  • 우선순위 큐를 사용하여 현재까지 발견된 최단 거리 중 가장 짧은 경로를 가진 정점을 반복적으로 선택합니다.
  • 선택된 정점의 인접 정점에 대해 최단 거리 기준으로 거리를 갱신합니다.
  • 모든 정점이 선택되거나, 남은 정점 모두의 거리가 무한대일 때까지 이 과정을 반복합니다.

이 과정을 통해 최종적으로 시작 정점에서 목표 정점까지의 최소 비용을 찾을 수 있습니다.

코틀린 코드 구현

이제 문제를 해결하기 위해 Dijkstra 알고리즘을 코틀린으로 구현해 보겠습니다.
먼저 필요한 라이브러리를 가져오고 입력을 처리한 후, 알고리즘을 구현하는 구조로 진행합니다.


import java.util.PriorityQueue

data class Edge(val to: Int, val cost: Int)

fun dijkstra(n: Int, edges: List>, start: Int): IntArray {
    val distance = IntArray(n + 1) { Int.MAX_VALUE }
    val priorityQueue = PriorityQueue>(compareBy { it.first })
    
    distance[start] = 0
    priorityQueue.add(0 to start)

    while (priorityQueue.isNotEmpty()) {
        val (currentDist, currentNode) = priorityQueue.poll()
        
        if (currentDist > distance[currentNode]) continue

        for (edge in edges[currentNode]) {
            val newDist = currentDist + edge.cost
            
            if (newDist < distance[edge.to]) {
                distance[edge.to] = newDist
                priorityQueue.add(newDist to edge.to)
            }
        }
    }

    return distance
}

fun main() {
    val (n, m) = readLine()!!.split(" ").map { it.toInt() }
    val edges = List(n + 1) { mutableListOf() }

    for (i in 1..m) {
        val (a, b, cost) = readLine()!!.split(" ").map { it.toInt() }
        edges[a].add(Edge(b, cost))
    }

    val (start, end) = readLine()!!.split(" ").map { it.toInt() }
    val distances = dijkstra(n, edges, start)

    println(if (distances[end] == Int.MAX_VALUE) -1 else distances[end])
}

코드 설명

위의 코드는 Dijkstra 알고리즘을 사용하여 문제를 해결하는 코틀린 프로그램입니다. 코드의 주요 구성 요소를 살펴보겠습니다.

  • Edge 데이터 클래스: 그래프의 간선을 표현하는 데이터 클래스입니다. 각 간선은 도착 정점과 비용 정보를 갖습니다.
  • dijkstra 함수: Dijkstra 알고리즘을 구현한 함수입니다. 입력으로 정점 수 n, 간선 리스트 edges, 시작 정점 start를 받아들입니다.
    최단 경로 정보를 저장할 distance 배열을 초기화하고, 우선순위 큐를 사용하여 경로를 탐색합니다.
  • main 함수: 입력을 처리하고 dijkstra 기능을 호출합니다. 최종적으로 목표 정점까지의 최소 비용을 출력합니다.
    만약 도달할 수 없는 경우에는 -1을 출력합니다.

테스트 케이스

코드의 기능을 확인하기 위해 몇 가지 테스트 케이스를 작성해 보겠습니다.
다양한 상황을 고려하여 코드의 성능과 정확성을 확인하는 것이 중요합니다.

테스트 케이스 1


입력:
5 6
1 2 2
1 3 3
2 4 1
3 4 4
2 5 5
4 5 1
1 5

출력:
8

테스트 케이스 2


입력:
3 2
1 2 2
1 3 4
2 3 5
1 3

출력:
4

테스트 케이스 3


입력:
4 3
1 2 10
2 3 5
3 4 10
1 4

출력:
-1

결론

이번 포스팅에서는 코틀린을 사용하여 최소 비용 구하기 문제를 해결하는 방법에 대해 알아보았습니다.
Dijkstra 알고리즘을 이용하여 그래프의 최단 경로를 효율적으로 찾아내는 방법을 설명하였고,
이를 코틀린으로 구현하였으며, 여러 테스트 케이스를 통해 코드의 작동을 확인했습니다.
알고리즘과 코틀린 프로그래밍에 대한 이해를 깊이 있게 다룰 수 있었던 시간이었습니다.
다음에도 유용한 알고리즘 문제와 솔루션으로 돌아오겠습니다.
감사합니다!

코틀린 코딩테스트 강좌, 최소 공통 조상 구하기 2

안녕하세요! 이번 시간에는 코틀린을 활용한 알고리즘 문제를 통해 최소 공통 조상(LCA, Lowest Common Ancestor) 구하기 문제를 해결해보겠습니다. 이 문제는 트리 구조를 다루는 중요한 개념 중 하나로, 다양한 코딩 테스트에서 등장합니다. 본 강좌는 주제에 대한 설명과 함께 문제 정의, 해결 과정, 그리고 최적화 방법까지 자세히 설명하겠습니다.

문제 정의

트리 구조가 주어졌을 때, 노드 A와 노드 B의 최소 공통 조상을 찾는 문제입니다. 최소 공통 조상은 노드 A와 노드 B의 조상 중에서 가장 깊은 조상을 의미합니다. 예를 들어, 아래와 같은 트리가 있다고 가정해보겠습니다.

            1
           / \
          2   3
         / \   \
        4   5   6
       /
      7

이 경우, 노드 4와 노드 5의 최소 공통 조상은 노드 2입니다. 또한, 노드 4와 노드 6의 최소 공통 조상은 노드 1입니다.

입력

  • 첫 번째 줄: 트리의 노드 개수 N (1 ≤ N ≤ 20,000)
  • 두 번째 줄부터 N-1 번째 줄: 두 개의 정수 a, b가 주어짐. 이는 a가 b의 부모임을 의미.
  • 마지막 줄: 두 개의 정수 X, Y (1 ≤ X, Y ≤ N) which are the nodes for which we need to find the LCA.

출력

최소 공통 조상의 노드 번호를 출력합니다.

예시 입력

7
1 2
1 3
2 4
2 5
4 7
3 6
4 6

예시 출력

1

문제 풀이 과정

이제 문제를 해결하기 위해 접근 방식을 설명하겠습니다. 트리 구조에서는 DFS(Depth-First Search)나 BFS(Breadth-First Search)를 활용하여 노드의 깊이를 저장하고 부모 노드를 기록하는 것이 일반적입니다. 이 방법을 통해 각 노드의 깊이를 비교하고, 공통 조상을 찾을 수 있습니다.

1. 트리 구축

우선, 입력으로 주어진 정보를 바탕으로 트리를 구축해야 합니다. 코틀린에서는 리스트를 사용하여 각 노드의 자식들을 저장하는 형태로 구현할 수 있습니다.


data class TreeNode(val id: Int) {
    val children = mutableListOf()
}

fun buildTree(n: Int, edges: List>): Map {
    val nodes = mutableMapOf()
    edges.forEach { (parentId, childId) ->
        val parentNode = nodes.getOrPut(parentId) { TreeNode(parentId) }
        val childNode = nodes.getOrPut(childId) { TreeNode(childId) }
        parentNode.children.add(childNode)
    }
    return nodes
}

2. 깊이 및 부모 노드 저장

DFS를 사용하여 깊이와 부모 노드를 저장하는 함수를 작성합니다. 이렇게 하면 나중에 LCA를 찾는 데 큰 도움이 됩니다.


val depth = mutableMapOf()
val parent = mutableMapOf()

fun dfs(node: TreeNode, dep: Int) {
    depth[node.id] = dep
    for (child in node.children) {
        parent[child.id] = node.id
        dfs(child, dep + 1)
    }
}

// 사용 예시
val root = nodes[1] // 루트 노드 (ID: 1)
dfs(root, 0)

3. LCA 찾기

이제 두 노드의 LCA를 찾는 함수를 구현합니다. 주어진 두 노드를 비교하여 깊이를 맞추고, 조상을 찾습니다.


fun findLCA(x: Int, y: Int): Int {
    // 깊이를 맞춤
    var a = x
    var b = y
    while (depth[a] > depth[b]) {
        a = parent[a]!!
    }
    while (depth[b] > depth[a]) {
        b = parent[b]!!
    }
    while (a != b) {
        a = parent[a]!!
        b = parent[b]!!
    }
    return a
}

// 사용 예시
val lca = findLCA(4, 6)
println("LCA: $lca")

4. 전체 구현

이제 모든 단계의 코드를 통합하여 전체 프로그램을 작성하겠습니다.


fun main() {
    val n = readLine()!!.toInt()
    val edges = mutableListOf>()
    repeat(n - 1) {
        val (a, b) = readLine()!!.split(" ").map { it.toInt() }
        edges.add(Pair(a, b))
    }
    val (x, y) = readLine()!!.split(" ").map { it.toInt() }

    val nodes = buildTree(n, edges)

    val depth = mutableMapOf()
    val parent = mutableMapOf()

    fun dfs(node: TreeNode, dep: Int) {
        depth[node.id] = dep
        for (child in node.children) {
            parent[child.id] = node.id
            dfs(child, dep + 1)
        }
    }

    val root = nodes[1]
    dfs(root, 0)

    fun findLCA(x: Int, y: Int): Int {
        var a = x
        var b = y
        while (depth[a] > depth[b]) {
            a = parent[a]!!
        }
        while (depth[b] > depth[a]) {
            b = parent[b]!!
        }
        while (a != b) {
            a = parent[a]!!
            b = parent[b]!!
        }
        return a
    }

    val lca = findLCA(x, y)
    println("LCA: $lca")
}

최적화

현재 방법은 DFS로 깊이와 부모 노드를 계산한 후 LCA를 구하는 방식입니다. 이 방식은 O(N)으로 동작하며, LCA를 단순히 찾는 데 O(log N)의 시간 복잡도를 가집니다. 하지만 보통 N이 커질 경우 최적화된 알고리즘인 “Sparse Table”이나 “Binary Lifting”을 활용할 수 있습니다. 이러한 방법들은 시간 복잡도를 더욱 줄일 수 있습니다.

결론

이번 강좌를 통해 최소 공통 조상을 찾는 방법에 대해 알아보았습니다. 트리 구조에서의 LCA 문제는 알고리즘 문제에서 자주 등장하는 주제이므로, 충분히 연습하여 몸에 익혀두는 것이 중요합니다. 각 단계별로 코드를 작성하고 이해하면서 연습해보세요. 다음 시간에는 다른 주제를 가지고 돌아오겠습니다. 감사합니다!

코틀린 코딩테스트 강좌, 최소 공통 조상 구하기 1

문제 설명

주어진 이진 트리에서 두 노드 A와 B의 최소 공통 조상(LCA, Lowest Common Ancestor)을 찾는 프로그램을 작성하시오. 최소 공통 조상은 두 노드 A와 B가 가지는 공통 조상 노드 중에서 가장 깊은 노드를 의미합니다.

입력 형식

이진 트리의 노드 개수 N (1 ≤ N ≤ 100,000)과 N개의 노드에 대한 정보가 주어진다. 각 노드 정보는 (부모, 왼쪽 자식, 오른쪽 자식) 형태로 주어진다. 마지막으로 두 노드 A와 B가 주어진다.

출력 형식

두 노드 A와 B의 최소 공통 조상 노드의 값을 출력한다.

예제 입력

7
1 2 3
2 4 5
3 6 7
4 -1 -1
5 -1 -1
6 -1 -1
7 -1 -1
4 5

예제 출력

2

문제 풀이 과정

문제를 해결하기 위해서는 다음과 같은 과정을 거쳐야 합니다:

1단계: 이진 트리 구성

먼저, 입력을 받아 이진 트리를 구성해야 합니다. 트리는 각 노드가 부모, 왼쪽 자식, 오른쪽 자식을 가지도록 구성될 것입니다. 이를 위해, 노드 클래스(Node)를 정의하고, 트리 구조를 저장할 수 있는 방식으로 구현합니다.


data class Node(val value: Int) {
    var left: Node? = null
    var right: Node? = null
}

2단계: 부모 트리 구성

트리를 구성할 때 각 노드의 부모를 추적할 수 있는 구조도 필요합니다. 따라서, 부모 노드를 모두 기록하여 어떤 노드가 부모인지 쉽게 찾을 수 있는 구조를 만듭니다.


val parentMap = mutableMapOf()

3단계: 두 노드의 경로 찾기

두 노드 A와 B의 경로를 찾아야 합니다. 이는 두 노드의 조상을 모아 각 노드가 각각의 경로를 따르게 만들어, 후에 최소 공통 조상을 찾는 데 유용합니다. 이를 위해 A와 B의 부모를 추적하며 경로를 저장합니다.


fun findPathToRoot(node: Int): List {
    val path = mutableListOf()
    var current = node
    while (current != -1) {
        path.add(current)
        current = parentMap[current] ?: -1
    }
    return path
}

4단계: 최소 공통 조상 찾기

A와 B 각각의 경로를 구한 후, 두 경로를 비교하여 가장 마지막에 만나는 조상을 찾습니다. 경로를 거꾸로 비교해서 최소 공통 조상을 찾으면 됩니다.


fun findLCA(nodeA: Int, nodeB: Int): Int {
    val pathA = findPathToRoot(nodeA)
    val pathB = findPathToRoot(nodeB)
    
    var lca = -1
    for (i in 0 until minOf(pathA.size, pathB.size)) {
        if (pathA[pathA.size - 1 - i] == pathB[pathB.size - 1 - i]) {
            lca = pathA[pathA.size - 1 - i]
        } else {
            break
        }
    }
    return lca
}

5단계: 전체 알고리즘 조합

위의 모든 단계를 조합하여 입력을 받아 실행하는 메인 함수를 작성합니다.


fun main() {
    val n = readLine()!!.toInt()
    for (i in 0 until n) {
        val (parent, left, right) = readLine()!!.split(" ").map { it.toInt() }
        if (parent != -1) {
            parentMap[parent] = parent
            if (left != -1) parentMap[left] = parent
            if (right != -1) parentMap[right] = parent
        }
    }
    val (a, b) = readLine()!!.split(" ").map { it.toInt() }
    
    val lca = findLCA(a, b)
    println(lca)
}

복잡도 분석

본 알고리즘의 시간 복잡도는 O(N)입니다. 이는 모든 노드를 한 번씩 방문하기 때문입니다. 공간 복잡도 또한 O(N)으로, 부모 관계를 저장하는 데 사용되는 공간 때문입니다. 이 알고리즘은 주어진 문제를 효율적으로 해결할 수 있습니다.

결론

이번 강좌에서는 이진 트리에서 최소 공통 조상을 찾는 방법에 대해 알아보았습니다. 이 알고리즘을 통해 트리 구조에 대한 이해도를 높일 수 있으며, 다양한 문제를 해결하는 데 도움을 줄 것입니다. 다음 강좌에서는 다른 유형의 트리 문제를 다룰 계획입니다. 감사합니다!