코틀린 코딩테스트 강좌, 최소 공통 조상 구하기 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)으로, 부모 관계를 저장하는 데 사용되는 공간 때문입니다. 이 알고리즘은 주어진 문제를 효율적으로 해결할 수 있습니다.

결론

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

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

이번 강좌에서는 “최소 공통 조상” 문제에 대해 알아보고, 코틀린을 사용하여 문제를 해결하는 방법에 대해 단계별로 설명하겠습니다.

문제 설명

주어진 이진 트리에서 두 노드 A와 B의 최소 공통 조상(LCA, Lowest Common Ancestor)을 찾는 문제입니다. 최소 공통 조상은 두 노드를 동시에 포함하는 가장 깊은 노드를 의미합니다.

예를 들어, 아래와 같은 이진 트리가 있다고 가정해 보겠습니다:

        1
       / \
      2   3
     / \
    4   5
    

이 경우 노드 4와 5의 최소 공통 조상은 노드 2입니다. 노드 2는 4와 5의 부모 노드이기 때문입니다.

입력 형식

입력으로는 이진 트리의 루트 노드와 두 개의 노드 A와 B가 주어집니다. A와 B는 트리의 노드 중 하나입니다.

출력 형식

출력은 노드 A와 B의 최소 공통 조상 노드입니다.

문제 해결 전략

  1. 재귀적인 접근: 이진 트리의 특성을 이용하여 트리를 재귀적으로 탐색합니다.
  2. 기저 조건: 현재 노드가 null이거나 현재 노드가 A 또는 B일 경우 그 노드를 반환합니다.
  3. 재귀 호출: 왼쪽 서브트리와 오른쪽 서브트리를 재귀적으로 탐색하게 됩니다.
  4. 조상 판단: 왼쪽과 오른쪽에서 각각 반환된 노드가 모두 null이 아닐 경우 현재 노드가 최소 공통 조상입니다.

코드 구현

이제 위의 알고리즘을 코틀린으로 구현해보겠습니다.


data class TreeNode(val value: Int, var left: TreeNode? = null, var right: TreeNode? = null)

fun lowestCommonAncestor(root: TreeNode?, A: TreeNode?, B: TreeNode?): TreeNode? {
    // 기저 조건
    if (root == null || root == A || root == B) {
        return root
    }

    // 왼쪽과 오른쪽 서브트리 탐색
    val left = lowestCommonAncestor(root.left, A, B)
    val right = lowestCommonAncestor(root.right, A, B)

    // 현재 노드가 LCA인 경우
    return when {
        left != null && right != null -> root // 서로 다른 서브트리에서 발견된 경우
        left != null -> left // 왼쪽 서브트리에서만 발견된 경우
        right != null -> right // 오른쪽 서브트리에서만 발견된 경우
        else -> null // 발견되지 않은 경우
    }
}
    

코드 설명

위의 코드는 다음의 주요 부분으로 나뉩니다:

  • data class TreeNode: 이진 트리의 노드를 정의하는 데이터 클래스입니다.
  • lowestCommonAncestor: 최소 공통 조상을 찾는 재귀 함수입니다.
  • 기저 조건을 확인한 후, 왼쪽 및 오른쪽 서브트리를 탐색하고, 조건에 따라 LCA를 반환합니다.

시간 복잡도 및 공간 복잡도

이 알고리즘의 시간 복잡도는 O(N)입니다. (N은 노드의 수) 모든 노드를 탐색하기 때문입니다.

공간 복잡도는 O(H)이며, H는 트리의 높이에 해당합니다. 이는 재귀 호출 스택에 의한 공간 사용량을 의미합니다.

예제 입력 및 출력

다음과 같은 입력을 고려해볼 수 있습니다:

    입력:
    트리:
        1
       / \
      2   3
     / \
    4   5
    A = 4
    B = 5

    출력:
    2
    

결론

이 강좌에서는 최소 공통 조상 문제의 정의와 해결 방법을 코틀린을 통해 자세히 살펴보았습니다. 이 문제는 다양한 상황에서 자주 발생하는 문제로, 이진 트리의 탐색 기술을 익히신다면 많은 알고리즘 문제를 해결하는 데 도움이 될 것입니다.

Note: 이 문제의 다양한 변형이나 추가 조건을 고려하여 연습해보면 더욱 좋습니다.

코틀린 코딩테스트 강좌, 최소 공배수 구하기

코딩테스트에서 자주 출제되는 알고리즘 문제 중 하나는 ‘최소 공배수(Least Common Multiple, LCM)’를 구하는 문제입니다. 최소 공배수는 두 수에서 각각의 배수를 구했을 때, 가장 작은 공통의 배수를 의미합니다. 이 글에서는 최소 공배수를 구하는 알고리즘을 코틀린으로 구현해보겠습니다.

문제 설명

정수 두 개 𝑎와 𝑏가 주어질 때, 이 두 수의 최소 공배수를 구하는 함수를 작성해주세요. 함수의 시그니처는 다음과 같습니다:

fun lcm(a: Int, b: Int): Int

예를 들어, 4와 6의 최소 공배수는 12이며, 15와 20의 최소 공배수는 60입니다. 입력 범위는 1 부터 106 사이의 정수입니다.

최소 공배수 구하기

최소 공배수는 두 수의 곱을 최대 공약수(Greatest Common Divisor, GCD)로 나누어 계산할 수 있습니다. 최소 공배수는 다음과 같은 식으로 정의됩니다:

LCM(a, b) = (a * b) / GCD(a, b)

위의 식을 사용하여 최소 공배수를 계산하기 위해서는 먼저 두 수의 최대 공약수를 구해야 합니다. 이를 위해 유클리드 호제법(Euclidean algorithm)을 사용할 수 있습니다.

유클리드 호제법 설명

유클리드 호제법은 두 수의 최대 공약수를 구하는 효율적인 방법입니다. 두 수 𝑎와 𝑏에 대해, 𝑏가 0이 아닐 때는 다음과 같이 진행합니다:

  1. 𝑎를 𝑏로 나눈 나머지를 구합니다.
  2. 𝑏를 𝑎에 대입합니다.
  3. 나머지를 𝑎에 대입합니다.
  4. 𝑏가 0이 될 때까지 반복합니다.

마지막으로 𝑎의 값을 최대 공약수로 반환합니다.

코틀린 구현

이제 위의 알고리즘을 기반으로 코틀린에서 최소 공배수를 계산하는 함수를 구현해보겠습니다.

fun gcd(a: Int, b: Int): Int {
        return if (b == 0) a else gcd(b, a % b)
    }

    fun lcm(a: Int, b: Int): Int {
        return (a * b) / gcd(a, b)
    }
    
    fun main() {
        val a = 4
        val b = 6
        println("최소 공배수: ${lcm(a, b)}")  // 출력: 최소 공배수: 12
    }

함수 설명

위의 코드는 두 가지 함수를 포함하고 있습니다:

  1. gcd(a: Int, b: Int): Int – 최대 공약수를 계산하는 함수입니다. 0이 될 때까지 나머지를 반복적으로 계산합니다.
  2. lcm(a: Int, b: Int): Int – 두 수의 곱을 최대 공약수로 나누어 최소 공배수를 계산하는 함수입니다.

복잡도 분석

유클리드 호제법의 시간 복잡도는 O(log min(a, b))입니다. 이는 두 수의 크기에 따라 훨씬 빠르게 최대 공약수를 계산할 수 있는 방법이 됩니다. 따라서, 최소 공배수를 구하는 전체 알고리즘의 시간 복잡도 또한 O(log min(a, b))입니다.

테스트 케이스

작업이 제대로 이루어졌는지 확인하기 위해 몇 가지 테스트 케이스를 살펴보겠습니다:

  • lcm(4, 6): 12
  • lcm(15, 20): 60
  • lcm(7, 5): 35
  • lcm(1, 999999): 999999
  • lcm(123456, 789012): 493827156

각 함수의 결과를 통해 로직의 정확성을 검증할 수 있습니다.

결론

코틀린을 사용하여 최소 공배수를 구하는 문제를 해결하는 방법을 배워보았습니다. 유클리드 호제법을 통해 최대 공약수를 효율적으로 계산하고, 이를 활용하여 최소 공배수를 구하는 과정을 살펴보았습니다. 이와 같은 알고리즘은 코딩테스트에서 빈번히 출제되므로, 반드시 숙지하고 연습하는 것이 좋습니다.

코틀린 코딩테스트 강좌, 최대 공약수 구하기

안녕하세요! 이번 시간에는 코틀린을 활용하여 최대 공약수를 구하는 알고리즘 문제를 다루어 보겠습니다. 최대 공약수(Greatest Common Divisor, GCD)란 두 개 이상의 정수가 공유하는 가장 큰 약수를 의미합니다. 이 문제는 프로그래밍 면접에서 자주 나오는 문제 중 하나입니다. 그럼 문제를 살펴보겠습니다.

문제 설명

주어진 두 정수 A와 B가 있을 때, 이 두 숫자의 최대 공약수를 계산하는 함수를 작성하세요. 예를 들어, A와 B가 각각 48과 18이라면, 최대 공약수는 6입니다.

입력 형식

  • 첫 번째 줄에 정수 A가 주어진다. (1 ≤ A ≤ 109)
  • 두 번째 줄에 정수 B가 주어진다. (1 ≤ B ≤ 109)

출력 형식

두 정수의 최대 공약수를 출력한다.

문제 접근 방법

최대 공약수를 구하는 방법에는 여러 가지가 있지만, 그 중에서도 유클리드 호제법(Euclidean algorithm)이 가장 유명하고 효율적인 방법입니다. 유클리드 호제법은 다음과 같은 원리를 기반으로 합니다:

  • GCD(A, B) = GCD(B, A % B) (B가 0이 될 때까지)
  • GCD(A, 0) = A

즉, 두 수 A와 B가 있을 때, B를 A로 나눈 나머지를 구하고, 이 과정에서 B가 0이 되는 경우 A가 최대 공약수임을 알 수 있습니다. 이 알고리즘은 두 수의 크기에 비례하여 매우 빠른 속도로 최대 공약수를 찾을 수 있습니다.

코틀린 구현

이제 위의 알고리즘을 코틀린으로 구현해 보겠습니다. 코드 예시는 아래와 같습니다:

fun gcd(a: Int, b: Int): Int {
    return if (b == 0) a else gcd(b, a % b)
}

fun main() {
    val a = readLine()!!.toInt()
    val b = readLine()!!.toInt()
    println(gcd(a, b))
}

위 코드는 두 정수를 입력받고 GCD 함수를 통해 최대 공약수를 계산하여 출력하는 방식입니다.

상세 코드 설명

1. 함수 정의

먼저, gcd이라는 이름의 함수를 정의합니다. 이 함수는 두 개의 정수 ab를 매개변수로 받습니다.

2. 종료 조건

함수 내부에서는 b가 0인지 확인합니다. 만약 b가 0이면 최대 공약수는 a이므로, a를 반환합니다.

3. 재귀 호출

그렇지 않으면, 재귀적으로 gcd(b, a % b)를 호출합니다. 이 과정을 반복하면서 b가 0이 될 때까지 최대 공약수를 계산합니다.

4. 메인 함수

main 함수에서는 readLine()을 사용하여 사용자로부터 두 개의 정수를 입력받습니다. 이 입력값은 !!를 이용해 null이 아님을 보장하고, toInt()를 호출하여 정수형으로 변환합니다. 그리고 최종적으로 println(gcd(a, b))를 호출하여 결과를 출력합니다.

테스트 케이스

이제 이 알고리즘이 제대로 작동하는지 테스트케이스를 통해 검증하겠습니다. 아래는 몇 가지 테스트 케이스입니다:

테스트 케이스 번호 A B Expected Output 실제 Output
1 48 18 6
2 56 98 14
3 101 10 1
4 7 13 1

코드 최적화

위의 구현은 간단하고 이해하기 쉬운 형태입니다. 그러나 조금 더 최적화된 방식으로 구현할 수도 있습니다. 코틀린의 내장 함수와 API를 사용하여, 더욱 줄어든 코드와 성능을 제공합니다. 예를 들어, 코틀린에서는 Math 클래스의 gcd 함수를 사용할 수도 있습니다. 이렇게 하면 코드를 더욱 간결하게 만들 수 있습니다:

fun main() {
    val a = readLine()!!.toInt()
    val b = readLine()!!.toInt()
    println(Math.gcd(a, b)) // Java 8 이상에서 사용하는 방법
}

마무리

이번 강좌에서는 코틀린을 사용하여 최대 공약수를 구하는 문제를 다루어 보았습니다. 유클리드 호제법을 통해 간단하면서도 효율적인 방법으로 해결할 수 있음을 알 수 있었습니다.

코딩 테스트 준비에 있어 다양한 문제를 접하고, 이 문제들을 해결하는 과정에서 알고리즘적 사고를 키우는 것이 중요합니다. 다음 코딩 테스트에 성공하기 위해서는 실제 코드를 구현하고, 다양한 상황을 테스트해 보시기 바랍니다.

추가적인 질문이나 주제에 대한 요청이 있다면 댓글로 남겨 주세요! 읽어주셔서 감사합니다!