코틀린 코딩테스트 강좌, 조약돌 꺼내기

문제 설명

조약돌 꺼내기 문제는 주어진 수의 조약돌을 특정 규칙에 따라 꺼내는 문제입니다.
각 조약돌은 특별한 무게를 가지고 있으며, 우리는 두 가지 규칙에 따라 꺼낼 수 있습니다.

  • 조약돌의 무게가 x 이하인 경우 꺼낼 수 있습니다.
  • 조약돌을 꺼내면 그 조약돌의 무게가 0이 되며, 주변의 조약돌에 영향을 미칩니다.

문제 입력

첫 번째 줄에는 조약돌의 개수 N (1 ≤ N ≤ 100,000)과 무게 제한 X (1 ≤ X ≤ 10,000)가 주어집니다.
두 번째 줄에는 각 조약돌의 무게가 공백으로 구분되어 주어집니다. (1 ≤ 무게 ≤ 10,000)

예시

        입력:
        5 5
        4 5 3 2 1

        출력:
        5
    

문제 해결 과정

이 문제를 해결하기 위해서는 다음과 같은 단계가 필요합니다:

1단계: 문제 분석

주어진 조약돌의 무게를 비교하는 문제입니다.
우리는 X 이하의 조약돌을 선택할 수 있으며, 이를 통해 몇 개의 조약돌을 꺼낼 수 있을지를 계산해야 합니다.
주어진 예제에서, 조약돌 무게는 [4, 5, 3, 2, 1]이고, X5입니다.
그래서 X 이하의 조약돌 무게(즉, 5 이하)를 가진 조약돌들은 5개입니다.
결과적으로, 우리는 5개의 조약돌을 꺼낼 수 있습니다.

2단계: 알고리즘 설계

문제를 해결하기 위해 사용할 수 있는 알고리즘은 다음과 같습니다:

  • 입력된 조약돌의 개수를 세고, 각 조약돌의 무게를 확인합니다.
  • 무게가 X 이하인 조약돌을 카운트합니다.
  • 최종적으로 카운트한 개수를 출력합니다.

3단계: 코틀린 코드 구현

아래는 위의 알고리즘을 코틀린으로 구현한 코드입니다:

        
        fun main() {
            val input = readLine()!!.split(" ").map { it.toInt() }
            val n = input[0]
            val x = input[1]

            val weights = readLine()!!.split(" ").map { it.toInt() }
            val count = weights.count { it <= x }

            println(count)
        }
        
    

4단계: 시간 복잡도 분석

이 알고리즘의 시간 복잡도는 O(N)입니다.
각각의 조약돌 무게를 확인해야 하므로, 입력받는 조약돌의 개수에 비례하여 시간 복잡도가 결정됩니다.

결론

조약돌 꺼내기 문제는 조건에 따라 데이터를 필터링하는 기초적인 알고리즘 문제입니다.
코틀린을 활용하면 이러한 문제를 간결하게 해결할 수 있으며, 기본적인 데이터 처리 기술을 연습하는 데에 좋은 학습 자료입니다.

코틀린 코딩테스트 강좌, 제곱이 아닌 수 찾기

안녕하세요! 이번 강좌에서는 코틀린을 이용해 코딩테스트에서 자주 접할 수 있는 알고리즘 문제를 깊이 있게 다뤄보겠습니다. 주제는 ‘제곱이 아닌 수 찾기’입니다. 본 문제의 기본 아이디어는 주어진 수 내에서 제곱수(1, 4, 9, 16 등)들이 아닌 모든 수를 찾아내는 것입니다. 이 문제를 이해하고 해결하기 위해 필요한 알고리즘적 사고와 코틀린 문법을 함께 살펴보겠습니다.

문제 설명

문제는 다음과 같습니다:

제곱이 아닌 수 찾기
1부터 N까지의 자연수 중에서 제곱수가 아닌 수들을 모두 찾는 함수를 작성하시오.

입력: 정수 N (1 ≤ N ≤ 1000)
출력: 제곱수가 아닌 수들의 리스트

문제 분석

이 문제를 해결하기 위해서는 다음과 같은 과정이 필요합니다:

  • 1부터 N까지의 모든 자연수를 반복문을 통해 탐색
  • 각 수가 제곱수인지 확인하는 방법 구상
  • 제곱수가 아닌 수들을 리스트에 저장

제곱수는 1, 4, 9, 16, 25, … 의 형태로 나타나며, 이러한 수들은 i의 제곱으로 표시됩니다. 그러므로 i가 1에서 시작하여 √N까지 증가하면서 그 값을 제곱한 결과를 리스트에 저장할 수 있습니다. 그 후, 이 제곱수들을 제외한 나머지 수들을 결과 리스트에 포함시키면 됩니다.

알고리즘 설계

위의 과정을 토대로 아래와 같은 알고리즘을 설계할 수 있습니다:

  1. 빈 리스트를 생성한다.
  2. 1부터 N까지 반복문을 돌린다.
  3. 각 수가 제곱수인지 판별한다.
  4. 제곱수가 아닐 경우 리스트에 추가한다.
  5. 결과 리스트를 출력한다.

코틀린 코드 구현

자 이제 이 알고리즘을 코틀린으로 구현해봅시다:

fun findNonPerfectSquares(n: Int): List {
    val nonPerfectSquares = mutableListOf()
    
    // 제곱수를 저장하기 위한 세트
    val perfectSquares = mutableSetOf()
    
    // 1부터 n의 제곱수 계산
    for (i in 1..Math.sqrt(n.toDouble()).toInt()) {
        perfectSquares.add(i * i)
    }
    
    // 1부터 N까지 탐색
    for (i in 1..n) {
        // 제곱수가 아닌 경우 리스트에 추가
        if (i !in perfectSquares) {
            nonPerfectSquares.add(i)
        }
    }
    
    return nonPerfectSquares
}

위 코드에서 findNonPerfectSquares 함수는 입력으로 주어진 N까지의 자연수 중 제곱수가 아닌 수들을 리스트로 반환합니다. mutableSetOf를 사용하여 제곱수를 효율적으로 관리하고, 탐색의 복잡성을 줄였습니다.

코드 실행 및 결과

이제 작성한 코드를 테스트해보고 결과를 확인해 보겠습니다. 예를 들어, N을 20으로 설정하고 함수를 호출하면 다음과 같은 결과를 얻을 수 있습니다:

fun main() {
    val n = 20
    val result = findNonPerfectSquares(n)
    println("1부터 $n까지의 제곱이 아닌 수들: $result")
}

위의 main 함수를 통해 결과를 출력하면 다음과 같습니다:

1부터 20까지의 제곱이 아닌 수들: [2, 3, 5, 6, 7, 8, 10, 11, 12, 13, 14, 15, 17, 18, 19, 20]

복잡도 분석

이번 문제의 시간복잡도를 살펴보면:

  • 제곱수를 계산하는 for문: O(√N)
  • N까지의 반복문에서 제곱수 체크: O(N)

따라서 전체 시간복잡도는 O(√N + N) = O(N)입니다.

정리

이번 강좌에서는 제곱이 아닌 수를 찾는 알고리즘 문제를 다뤘습니다. 문제의 구조를 이해하고 이를 코틀린으로 구현하면서 제곱수의 개념과 효율적으로 데이터를 관리하는 방법에 대해서도 배웠습니다. 코딩테스트에서 자주 출제되는 문제가 유형인 만큼 실제 문제를 풀어가는 과정처럼 접근하는 것이 중요합니다. 궁금한 점이나 추가적으로 알고 싶은 내용이 있다면 댓글로 남겨주세요! 앞으로도 유익한 알고리즘 강좌를 기대해 주세요!

코틀린 코딩테스트 강좌, 절댓값 힙 구현하기

코틀린(Kotlin)은 현대적이고 간결한 문법과 강력한 기능 덕분에 최근 많은 개발자들 사이에서 사랑받고 있는 프로그래밍 언어입니다. 코딩 테스트, 즉 알고리즘 문제 풀이에서도 코틀린은 그 유용함을 발휘합니다. 이번 포스트에서는 코틀린을 사용하여 절댓값 힙(Absolute Value Heap)을 구현하는 방법에 대해 상세히 설명하고자 합니다.

문제 설명

절댓값 힙의 정의는 다음과 같습니다:

절댓값 힙은 항상 절댓값이 가장 작은 값을 상단에 두는 우선순위 큐(Priority Queue)입니다.
만약 절댓값이 같은 값이 여러 개 존재하는 경우, 그 값 중 실제 값이 더 작은 것을 우선시합니다.

힙의 연산은 다음과 같습니다:

  • INSERT X: 정수 X (우선순위 큐에 추가)
  • EXTRACT: 절댓값이 가장 작은 정수 삭제 후 출력

입력 형식

프로그램의 입력은 여러 줄로 주어지며, 각 줄에 대한 명령이 포함되어 있습니다.
각 줄은 ‘INSERT X’ 또는 ‘EXTRACT’ 형태로 주어집니다.

출력 형식

‘EXTRACT’ 명령이 호출될 때마다 출력된 값이 한 줄에 출력됩니다.

예제


    INPUT:
    INSERT -5
    INSERT 3
    INSERT -1
    EXTRACT
    EXTRACT
    INSERT -2
    EXTRACT
    EXTRACT
    EXTRACT
    

OUTPUT:
-1
-2
3

문제 분석

주어진 문제를 해결하기 위해 우리는 다음을 고려해야 합니다:

  • 우선순위 큐(Priority Queue)를 어떻게 구성할 것인가?
  • Java의 힙 구조를 활용할까? 아니면 코틀린의 컬렉션을 활용할까?
  • 절댓값에 따라 정렬하는 방법은 무엇인가?

절댓값 힙을 만들기 위해서는 최소 힙(min-heap) 구조를 사용할 수 있습니다.
자바에서는 PriorityQueue를 사용하여 이를 쉽게 구현할 수 있습니다.
코틀린에서도 이를 활용할 수 있으며, 기본적인 힙 구조를 통해 우리가 원하는 기능을 구현할 수 있습니다.

구현 과정

1. 데이터 구조 정의

절댓값 힙에서 가장 핵심적인 부분은 데이터를 저장하는 방법입니다.
우리는 절댓값을 기준으로 정렬해야 하므로, PriorityQueue를 활용하여 요소를 추가하고 제거합니다.
요소를 추가할 때는 절댓값과 원래 값을 함께 설정해주는 것이 좋습니다.

2. Comparator 정의

PriorityQueue에서 우선순위를 정의하기 위해 커스텀 정렬 규칙(Comparator)을 정의합니다.
여기서는 절댓값이 작은 값이 먼저 와야 하며, 절댓값이 같을 경우 실제 값이 더 작은 값을 우선시해야 합니다.


    val comparator = Comparator> { a, b ->
        if (Math.abs(a.first) == Math.abs(b.first)) {
            a.first.compareTo(b.first)
        } else {
            Math.abs(a.first).compareTo(Math.abs(b.first))
        }
    }
    

3. 명령 처리

입력된 명령을 처리하기 위해 루프를 실행합니다.
명령이 INSERT X일 경우 힙에 값을 추가하고, EXTRACT 명령일 경우 힙에서 값을 제거하고 출력합니다.


    val priorityQueue = PriorityQueue>(comparator)

    val reader = BufferedReader(InputStreamReader(System.`in`))
    val n = reader.readLine().toInt()
    
    repeat(n) {
        val command = reader.readLine().split(" ")
        when (command[0]) {
            "INSERT" -> {
                val value = command[1].toInt()
                priorityQueue.add(Pair(value, value))
            }
            "EXTRACT" -> {
                if (priorityQueue.isNotEmpty()) {
                    println(priorityQueue.poll().first)
                } else {
                    println(0)
                }
            }
        }
    }
    

전체 코드


    import java.io.BufferedReader
    import java.io.InputStreamReader
    import java.util.PriorityQueue

    fun main() {
        val comparator = Comparator> { a, b ->
            if (Math.abs(a.first) == Math.abs(b.first)) {
                a.first.compareTo(b.first)
            } else {
                Math.abs(a.first).compareTo(Math.abs(b.first))
            }
        }

        val priorityQueue = PriorityQueue>(comparator)

        val reader = BufferedReader(InputStreamReader(System.`in`))
        val n = reader.readLine().toInt()

        repeat(n) {
            val command = reader.readLine().split(" ")
            when (command[0]) {
                "INSERT" -> {
                    val value = command[1].toInt()
                    priorityQueue.add(Pair(value, value))
                }
                "EXTRACT" -> {
                    if (priorityQueue.isNotEmpty()) {
                        println(priorityQueue.poll().first)
                    } else {
                        println(0)
                    }
                }
            }
        }
    }
    

결론

절댓값 힙을 구현하는 것은 동적 데이터 구조의 중요한 부분으로, 다양한 문제 해결에 도움을 줄 수 있습니다.
이번 강좌를 통해 코틀린에서 간단하고 효과적으로 힙을 구현하는 방법을 배웠습니다.
알고리즘 문제를 풀 때, 문제의 본질을 이해하고 효율적인 데이터 구조를 선택하는 것이 얼마나 중요한지를 느낄 수 있었기를 바랍니다.
앞으로도 다양한 알고리즘 문제를 함께 풀어보며 실력을 향상시켜 나가길 기대합니다!

코틀린 코딩테스트 강좌, 정수를 1로 만들기

안녕하세요, 코딩테스트를 준비하고 있는 여러분! 오늘은 정수를 1로 만드는 문제를 함께 풀어보겠습니다. 이 문제는 알고리즘의 기초를 다지기에 아주 좋은 연습문제이며, 코틀린을 사용하여 해결해보도록 하겠습니다.

문제 설명

정수 N이 주어질 때, N을 1로 만드는 최소의 연산 횟수를 구하는 문제입니다. 사용할 수 있는 연산은 아래와 같습니다:

  • N이 3으로 나누어 떨어지면, N을 3으로 나눈다.
  • N이 2로 나누어 떨어지면, N을 2로 나눈다.
  • 1을 빼준다.

예를 들어, N이 10일 경우, 10 -> 9 -> 3 -> 1의 과정으로 3번의 연산이 필요합니다.

문제 접근 방법

이 문제를 해결하기 위해 다이나믹 프로그래밍(DP) 기법을 사용할 것입니다. DP는 기존의 문제들을 잘게 나누어 푸는 방식입니다. 이 문제의 경우, N을 1로 만들기 위한 각 숫자의 최소 연산 횟수를 저장하는 배열을 만들어 사용할 것입니다.

단계별 접근

  1. 배열 초기화: 정수 N의 크기만큼 배열을 만들고, 각 인덱스에 기본값으로 -1로 초기화합니다.
  2. 기저 사례 설정: 배열의 1번 인덱스에는 0을 저장합니다. 이는 1을 만드는 데 필요한 연산이 0임을 나타냅니다.
  3. 반복문을 통한 DP 테이블 업데이트: 2부터 N까지 반복문을 통해 각 숫자에 대해 최소 연산 횟수를 계산합니다. 이때, 나누기 연산과 빼기를 통해 새로운 값을 계산하고 이를 배열에 저장합니다.
  4. 결과 출력: 마지막에 계산한 N의 값을 메인 배열에서 가져와 출력합니다.

코틀린 코드


fun minOperationsToOne(n: Int): Int {
    // 다이나믹 프로그래밍 테이블 초기화
    val dp = IntArray(n + 1) { Int.MAX_VALUE }
    dp[1] = 0 // 1은 1로 만들기 위해 0개의 연산 필요

    for (i in 2..n) {
        // 1 빼기
        dp[i] = dp[i - 1] + 1
        
        // 2로 나누기
        if (i % 2 == 0) {
            dp[i] = minOf(dp[i], dp[i / 2] + 1)
        }
        
        // 3으로 나누기
        if (i % 3 == 0) {
            dp[i] = minOf(dp[i], dp[i / 3] + 1)
        }
    }
    return dp[n]
}

fun main() {
    val n = 10 // 테스트 입력
    println("정수를 1로 만드는 최소 연산 횟수: ${minOperationsToOne(n)}")
}
    

코드 설명

위 코드는 주어진 정수 N을 1로 만드는 최소 연산 횟수를 계산합니다. minOperationsToOne 함수는 정수를 입력받아 동적 프로그래밍을 통해 최소 연산 횟수를 구합니다. 각 연산별로 조건을 체크하여 가능한 최소 횟수를 업데이트합니다. 특히 2로 나누기와 3으로 나누기는 조건문으로 체크하여 적절한 경우에만 연산을 수행합니다.

실행 결과

위의 코드를 실행시키면, 주어진 입력 N에 대해 정수를 1로 만들기 위한 최소 연산 횟수가 출력됩니다. 예를 들어, N이 10인 경우, ‘정수를 1로 만드는 최소 연산 횟수: 3’이 출력됩니다.

시간 복잡도 분석

이 알고리즘의 시간 복잡도는 O(N)입니다. 배열을 한 번 스캔하며 각 연산에 대해 상수 시간의 작업을 하기 때문에 효율적인 계산이 가능합니다. 공간 복잡도는 O(N)으로 DP 배열을 저장하기 위한 공간이 필요합니다.

결론

오늘은 정수를 1로 만드는 문제를 다루었습니다. 코틀린을 사용하여 문제를 풀면서 다이나믹 프로그래밍의 기초를 익힐 수 있었습니다. 이와 같은 유형의 문제는 취업 준비과정에서 자주 출제되므로, 꾸준히 연습하시는 것을 추천드립니다. 다음 강좌에서도 유익한 내용을 가지고 돌아오겠습니다!

참고 자료

감사합니다!

코틀린 코딩테스트 강좌, 임계 경로 구하기

작성일: 2023년 10월 1일

작성자: 조광형

문제 정의

주어진 연결된 그래프에서 시작 노드부터 도착 노드까지의 임계 경로를 구하는 문제입니다. 임계 경로란 그래프에서 요구되는 작업을 수행하는 데 필요한 최소 시간 또는 거리를 나타내며, 프로젝트 관리 및 자원의 최적 배분에 중요한 역할을 합니다.

예를 들어, 각 작업이 완료되는 데 걸리는 시간을 나타내는 다이아그램이 주어졌다고 가정합니다. 각 작업이 완료되기 위해서는 다른 작업들이 먼저 완료되어야 하는 조건이 있을 수 있습니다. 이러한 조건을 반영한 Directed Acyclic Graph (DAG)를 활용하여 문제를 해결할 수 있습니다.

입력 형식

  • 첫 번째 줄에 자연수 N (작업의 수)이 주어진다.
  • 다음 N개의 줄에는 작업의 번호, 소요 시간, 의존 작업의 번호가 공백으로 구분되어 주어진다. 의존 작업은 없으면 -1로 표시된다.

출력 형식

임계 경로의 길이 (총 소요 시간)를 출력한다.

예시

입력

            5
            1 3 -1
            2 2 1
            3 1 1
            4 4 2
            5 2 3
            

출력

            9
            

문제 풀이 과정

이 문제를 해결하기 위해서는 다음과 같은 과정을 따릅니다:

  1. 그래프 모델링: 각 작업을 노드로, 의존 관계를 간선으로 표현하여 Directed Acyclic Graph (DAG)를 생성합니다.
  2. 위상 정렬: 간선을 따라 작업을 수행하기 위해서는 작업의 의존성을 고려해야 합니다. 이를 위해 위상 정렬을 수행하여 노드를 정렬합니다.
  3. 최장 경로 계산: 위상 정렬 결과를 따라 각 작업의 소요 시간을 누적하여 최장 경로를 계산합니다. 노드가 가지는 소요 시간의 최대값을 저장하여 최종 결과를 도출합니다.

코틀린 코드 구현

            fun main() {
                val n = readLine()!!.toInt()
                val time = IntArray(n + 1)
                val graph = Array(n + 1) { mutableListOf() }
                val indegree = IntArray(n + 1)
                
                for (i in 1..n) {
                    val input = readLine()!!.split(" ").map { it.toInt() }
                    time[i] = input[1]
                    if (input[2] != -1) {
                        graph[input[2]].add(i)
                        indegree[i]++
                    }
                }

                val dp = IntArray(n + 1)
                val queue: Queue = LinkedList()

                for (i in 1..n) {
                    if (indegree[i] == 0) {
                        queue.offer(i)
                        dp[i] = time[i]
                    }
                }

                while (queue.isNotEmpty()) {
                    val current = queue.poll()
                    for (next in graph[current]) {
                        dp[next] = maxOf(dp[next], dp[current] + time[next])
                        indegree[next]--
                        if (indegree[next] == 0) {
                            queue.offer(next)
                        }
                    }
                }

                println(dp.maxOrNull())
            }
            

코드 설명

위 코드는 아래와 같은 단계로 구성되어 있습니다:

  1. 작업의 수 N을 입력받습니다.
  2. 작업의 실행 시간을 저장하는 time 배열과 그래프 간선을 저장하는 graph 배열을 초기화합니다.
  3. 각 작업의 의존 관계를 읽어들여 그래프와 진입 차수를 설정합니다.
  4. Queue를 사용하여 위상 정렬을 수행하며 최장 경로를 계산합니다. 각 작업의 소요 시간을 dp 배열에 누적합니다.
  5. 최장 경로의 결과를 출력합니다.

문제 해결 전략

임계 경로 구하기 문제는 프로젝트 관리에서 시간 관리를 위한 중요한 요소입니다. 따라서 이러한 문제를 해결하기 위해서는:

  • 코드 최적화: 성능을 고려하여 알고리즘을 최적화하는 것이 중요합니다.
  • 분석 및 계획: 문제 요구 사항을 체계적으로 분석하고 해결 방안을 계획하는 것이 필수적입니다.
  • 유사 문제 연습: 유사한 문제를 통해 경험을 쌓고 다양한 상황을 고려하는 연습이 필요합니다.

이 글은 코틀린을 활용한 알고리즘 문제풀이 강좌의 일환입니다. 추가적인 질문이나 피드백은 언제든지 환영합니다!