https://www.acmicpc.net/problem/9466

 

9466번: 텀 프로젝트

이번 가을학기에 '문제 해결' 강의를 신청한 학생들은 텀 프로젝트를 수행해야 한다. 프로젝트 팀원 수에는 제한이 없다. 심지어 모든 학생들이 동일한 팀의 팀원인 경우와 같이 한 팀만 있을

www.acmicpc.net

 

풀이

문제를 이해하기 위해 예제를 그림으로 그려보았다.

 

어디서 시작해도 사이클이 생긴다

 

팀을 이루기 위해선 각 학생들이 선택한 학생들을 따라갔을 때, 사이클이 형성되어야 한다는 걸 알 수 있다. 그리고 사이클은 어디서 시작하든지 반드시 생긴다. 사이클이 생기지 않으려면 중간에 화살표가 끊기거나 화살표가 무한히 이어져야 하는데, 그럴려면 팀원을 선택하지 않은 학생이 있거나 학생 수가 무한히 많아야 하기 때문이다.

 

또한, 2 -> 1 -> 3 -> 3 처럼 중간에 다른 사이클이 생기는 학생이 팀에 속하지 않는 학생이 된다.

 

그래서 나는 각 학생이 선택한 학생을 배열에 저장한 후, dfs를 사용해서 사이클을 만드는 학생들의 수를 구했다. 그리고 전체 학생 수에서 빼주는 방식으로 답을 구했다.

 

구현 방식

(1) 모든 학생을 한번씩 시작점으로 탐색해서, 자기 자신으로 돌아오는지 확인

가장 간단해 보이는 건 모든 학생을 시작 지점으로 탐색해서 자기 자신으로 돌아오는지 확인하는 방법인데, 이렇게 하면 시간 초과가 발생한다. 탐색에 걸리는 시간은 O(n), 모든 학생마다 탐색하면 O(n^2) = 100억이 되기 때문이다.

 

그래서 그래프 탐색에서 시간을 줄이는 방법인, 한번 확인한 학생은 다시 확인하지 않도록 해야 문제를 해결할 수 있다. 

한번 방문했던 학생이 다시 나오면, 이전의 탐색 과정이 그대로 등장한다는 점을 이용한다.

(ex) 1 -> 2  -> 3 -> 1 을 탐색하고 나서, 4 -> 1이 나온다면 1 -> 2 -> 3 -> 1 탐색을 다시 반복하게 된다.)

 

(2) 중복 체크 자료구조 2개 사용 

확인한 학생은 다시 방문하지 않도록 해야 하는데, 문제는 사이클을 이루는 학생이 다시 나타났을 때 방금 탐색하다가 확인한 건지, 이전 탐색에서 확인한 건지 알 수가 없다는 것이다.

 

그래서 방문 체크하는 배열을 2개 사용하였다. 

- finished: 사이클을 형성 여부까지 확인했는지 체크

- visited: 한 번이라도 방문 했는지 체크

 

알고리즘) dfs (재귀 or 스택)

  1. 처음 방문하는 학생일 경우 visited에 체크하고 그 학생이 선택한 학생을 방문한다.
  2. 다시 방문하는 학생일 경우,
    1. finished가 체크된 학생 -> 이전의 탐색 과정이 그대로 나오므로 탐색을 종료한다.
    2. 그렇지 않다면, 사이클을 형성하는 학생이므로, 그 학생의 다음 학생부터 자기 자신으로 되돌아올 때까지 반복문을 돌리면서 사이클을 형성하는 학생 카운트를 증가시킨다.
  3. 함수나 스택을 빠져나가면서 사이클까지 모두 확인했음을 finished에 체크한다.

 

(3) 방문 확인 배열 + 탐색을 시작할 때마다 새로운 중복 체크 자료구조를 생성해서 사용

새 탐색을 시작할 때마다 추가 자료구조 사용

탐색 과정에서 이전에 사이클 여부까지 확인했는지를 필터링할 수는 있지만, 새로운 자료구조를 생성할 때마다 O(n)의 시간복잡도가 더 필요해서 결국 O(n^2)으로 시간 초과가 발생한다.

 

나는 여기서 많이 해맸었다..ㅋㅋ

 

코드1 (방식 2)

import java.io.BufferedReader
import java.io.InputStreamReader
import java.util.*

lateinit var graph: IntArray
lateinit var visited: BooleanArray
lateinit var finished: BooleanArray
var count = 0

fun main() = with(BufferedReader(InputStreamReader(System.`in`))) {
    val t = readLine().toInt()
    val answer = StringBuilder()

    repeat(t){
        val n = readLine().toInt()
        val st = StringTokenizer(readLine())
        count = 0
        graph = IntArray(n+1)
        visited = BooleanArray(n+1)
        finished = BooleanArray(n+1)

        for (i in 1..n){
            graph[i] = st.nextToken().toInt()
        }

        for (i in 1..n){
            if (visited[i].not()) dfs(i)
        }

        answer.append(n - count).appendLine()
    }

    print(answer)
}

fun dfs(student: Int){
    // 처음 방문한 학생
    if (visited[student].not()) {
        visited[student] = true // 방문 체크
        dfs(graph[student])     // 다음 학생 방문
    }
    else{
        if (finished[student]) return // 사이클 형성 여부까지 이미 확인한 학생
        else{
            var nextStudent = graph[student] // 자신부터 시작하면 while문이 안돌아간다

            while (nextStudent != student){
                count++ // 사이클을 형성하는 학생 수 증가
                nextStudent = graph[nextStudent]
            }
            count++ // 자기 자신도 센다 ex) 2 -> 1 -> 3 -> 2: 반복문에서 1,3을 세고 마지막에 2를 센다

            return
        }
    }

    finished[student] = true // 사이클 형성 여부까지 확인했음
}

 

코드2 (방식2와 개념은 같지만 추가 방문 체크용 자료 구조로 Map을 사용했습니다)

import java.io.BufferedReader
import java.io.InputStreamReader
import java.util.*

lateinit var graph: IntArray
lateinit var visited: BooleanArray
lateinit var finished: BooleanArray

fun main() = with(BufferedReader(InputStreamReader(System.`in`))) {
    val t = readLine().toInt()
    val answer = StringBuilder()

    repeat(t){
        val n = readLine().toInt()
        val st = StringTokenizer(readLine())
        var count = 0
        graph = IntArray(n+1)
        visited = BooleanArray(n+1)
        finished = BooleanArray(n+1)

        for (i in 1..n){
            graph[i] = st.nextToken().toInt()
        }

        for (i in 1..n){
            if (!visited[i]){
                count += dfs(i, mutableListOf(), HashMap<Int, Int>())
                // updateGroup(hasGroup, students, startIndex)
            }
        }

        answer.append(n - count).appendLine()
    }

    print(answer)
}

// Map을 추가 방문 체크 자료구조로 사용했습니다
fun dfs(student: Int, studentList: MutableList<Int>, indexMap: MutableMap<Int, Int>): Int {
    if (finished[student]) return 0 // 사이클 형성 여부가 이미 확인된 학생
    if (visited[student]) {         // 사이클이 형성됨
        return studentList.size - indexMap[student]!!
    }

    visited[student] = true
    indexMap[student] = studentList.size
    studentList.add(student)
    val count = dfs(graph[student], studentList, indexMap)

    finished[student] = true
    return count
}

https://www.acmicpc.net/problem/1406

 

1406번: 에디터

첫째 줄에는 초기에 편집기에 입력되어 있는 문자열이 주어진다. 이 문자열은 길이가 N이고, 영어 소문자로만 이루어져 있으며, 길이는 100,000을 넘지 않는다. 둘째 줄에는 입력할 명령어의 개수

www.acmicpc.net

 

풀이

자료구조에 관한 문제이다. 문자열을 어딘가에 저장해야 하는데, 커서의 개념이 존재하고 그걸 움직일 수도 있다. 커서에 방향이 있으므로 데이터에 순서가 있고, 추가/삭제가 쉬운 자료구조를 사용해야 한다. 따라서, 배열은 사용할 수 없고, 추가/삭제가 빠르고 데이터에 순서가 있는 연결 리스트를 생각해볼 수 있다. 

 

커서는 어떻게 구현할 수 있을까?

 

방법1) 연결 리스트

연결리스트를 응용하면 커서의 개념을 구현할 수 있다. 연결 리스트를 2개를 사용해서 커서의 왼쪽 리스트와 오른쪽 리스트의 개념으로 사용하면, 왼쪽 리스트의 끝과 오른쪽 리스트의 시작 지점 사이에 자연스럽게 커서가 생긴다. 그러면 왼쪽 리스트의 끝에 데이터를 추가/삭제할 수 있다.

ex) (왼쪽 리스트: a-b-c-d-x) - 커서 - (오른쪽 리스트: y) 

 

방법2) 스택

연결 리스트와 마찬가지로 왼쪽 스택과 오른쪽을 스택을 사용해서 구현할 수 있다. 양 스택의 꼭대기에 있는 데이터를 움직여서 커서를 움직이고, 왼쪽 스택의 pop/push로 커서 왼쪽에 데이터를 추가/삭제할 수 있다.

연결리스트와 차이점은 실제 문자열의 순서가 스택에 저장된 순서와 조금 다르다는 것이다. 문자열을 왼쪽에서 오른쪽으로 읽을 때, 왼쪽 스택의 바닥에서 꼭대기, 오른쪽 스택의 꼭대기에서 바닥 순서로 읽어야 한다.

 

방법3) listIterator

다른 사람들의 코드를 읽어보다가 문제에서 요구한 것과 완벽하게 동일한 자료구조가 자바에 존재한다는 걸 알게됐다. listIterator라는 녀석이다. Iterator 인터페이스를 상속한 것으로 Collection의 List에 사용할 수 있는데, 컬렉션의 요소에 접근, 추가, 삭제 등을 지원한다. Iterator는 정방향으로만 순회가 가능하지만 listIterator는 역방향으로도 순회가 가능하고 커서도 존재한다ㅋㅋ. 주의할 점은, 원래의 컬렉션 객체를 참조하기 때문에 Iterator에서 데이터를 추가/삭제하면 원래 객체에도 그대로 반영된다.

참고) http://www.tcpschool.com/java/java_collectionFramework_iterator 여기에 자세하게 설명되어 있다.

 

 

코드1 (연결 리스트)

import java.io.*
import java.util.*

fun main() = with(BufferedReader(InputStreamReader(System.`in`))){
    // 커서 왼쪽에 문자들 추가
    val leftList = LinkedList<String>().apply{ addAll(readLine().map{ it.toString() }) }
    val rightList = LinkedList <String>()
    val n = readLine().toInt()

    repeat(n){
        val cmd = readLine()

        when(cmd){
            "L" -> {
                if (leftList.isNotEmpty()){ // 커서 왼쪽에 문자가 있을 때
                    rightList.addFirst(leftList.pollLast())
                }
            }
            "D" -> {
                if (rightList.isNotEmpty()){ // 커서 오른쪽에 문자가 있을 때
                    leftList.addLast(rightList.pollFirst())
                }
            }
            "B" -> {
                if (leftList.isNotEmpty()){
                    leftList.removeLast() // 커서 왼쪽에 있는 문자 삭제
                }
            }
            else -> {
                leftList.add(cmd[2].toString()) // 커서 왼쪽에 문자 추가
            }
        }
    }

    val answer = StringBuilder()
    leftList.forEach { answer.append(it) } // 연결리스트는 순서대로 문자를 읽을 수 있다.
    rightList.forEach{ answer.append(it) }
    print(answer)
}

 

코드2 (스택: 리스트 사용)

import java.io.*
import java.util.*

fun main() = with(BufferedReader(InputStreamReader(System.`in`))){
    val leftStack = readLine().map{ it.toString() }.toMutableList()
    val rightStack = mutableListOf<String>() // 리스트로 스택 구현
    val n = readLine().toInt()

    repeat(n){
        val cmd = readLine()

        when(cmd){
            "L" -> {
                if (leftStack.isNotEmpty()){
                    rightStack.add(leftStack.removeAt(leftStack.lastIndex))
                }
            }
            "D" -> {
                if (rightStack.isNotEmpty()){
                    leftStack.add(rightStack.removeAt(rightStack.lastIndex))
                }
            }
            "B" -> {
                if (leftStack.isNotEmpty()){
                    leftStack.removeAt(leftStack.lastIndex)
                }
            }
            else -> {
                leftStack.add(cmd[2].toString())
            }
        }
    }

    val answer = StringBuilder()
    leftStack.forEach { answer.append(it) }
    for (i in rightStack.lastIndex downTo 0){ // 오른쪽 스택은 top부터 문자를 읽어야 한다
        answer.append(rightStack[i])
    }
    print(answer)
}

 

코드3 (listIterator)

import java.io.BufferedReader
import java.io.InputStreamReader
import java.util.*

fun main() = with(BufferedReader(InputStreamReader(System.`in`))){
    val str = readLine()
    val list = LinkedList<String>()
    str.forEach { 
        list.add(it.toString())
    }
    val iter = list.listIterator(list.size)

    val n = readLine().toInt()
    repeat(n){
        val cmd = readLine()

        when(cmd){
            "L" -> {
                if (iter.hasPrevious()){ // 커서 왼쪽에 데이터가 있으면
                    iter.previous()      // 그걸 리턴하고 커서를 반환할 데이터의 왼쪽으로 움직인다.
                }
            }
            "D" -> {
                if (iter.hasNext()){ // 커서 오른쪽에 데이터가 있으면
                    iter.next()      // 그걸 리턴하고 커서를 반환할 데이터의 오른쪽으로 움직인다.
                }
            }
            "B" -> {
                if (iter.hasPrevious()){
                    iter.previous()
                    iter.remove()   // 가장 최근에 리턴한 데이터를 삭제한다 -> 실제 리스트에도 반영된다
                }
            }
            else -> {
                iter.add(cmd[2].toString()) // 커서 위치에 데이터 추가 -> 리스트에 반영
            }
        }
    }

    val answer = StringBuilder()
    list.forEach {
        answer.append(it)
    }
    print(answer)
}

https://www.acmicpc.net/problem/2225

 

2225번: 합분해

첫째 줄에 답을 1,000,000,000으로 나눈 나머지를 출력한다.

www.acmicpc.net

 

풀이

이전 항으로 다음 항의 값을 구할 수 있는 다이나믹 프로그래밍 문제이다.

 

문제의 상황을 수식으로 나타내보면 다음과 같다.

 

 

이렇게 되는 경우의 수를 dp[k][N]이라고 할 떄, 위에 수식은 아래처럼 다시 바꿔서 표현할 수 있다.

 

 

L이라는 값을 미리 더해서 (0 ~ n-L)까지 k-1개의 숫자를 더해서 N-L을 만드는 경우의 수를 구하는 상황으로 바꿀 수 있고, k-1개로 묶인 부분이 dp[k-1][N-L]이 된다.

 

그러면 dp[k][N] = sum( dp[k-1][N-L] ) (0 <= L <= N)이라는 점화식을 만들어낼 수 있는데, 변수가 3개이므로 3중 반복문을 사용해서 구현할 수 있다.

 

 

dp[k][N]의 식을 조금 살펴보면,

k = 1일 때 즉, 숫자 하나로 N을 만드는 경우의 수는 N이 무엇이든 항상 N 하나이므로 dp[1][N] = 1이다. 그리고 이게 초기 값이 된다.

n =0일 때는 숫자가 몇 개든지 0만 더해지는 경우밖에 없다. 따라서 dp[k][0] = 1이다. 

ex) dp[4][0] = 1 (0 + 0 + 0 +0)

그리고 이것들이 dp의 초기 값이 된다.

 

머리로만 하면 이해가 잘 안될 수도 있어서 직접 해보는 게 좋다. 내가 그랬다ㅋ_ㅋ

 

ex) n = 6, k = 4일 때 반복문 결과

dp[k][n] n = 0 1 2 3 4 5 6
k =1 1 1 1 1 1 1 1
2 1 1+1=2 1+1+1=3 4 5 6 7
3 1 1+2=3 1+2+3=6 10 15 21 28
4 1 1+3=4 1+3+6=10 20 35 56 56+28=84

  

계산하다 보면 dp[k][n] = dp[k][n-1] + dp[k-1][n] 이라는 특이한 구조를 발견할 수 있다. 이 점화식을 사용하면 변수가 두 개라서 이중반복문으로 좀 더 빠르게 구현이 가능하다!

 

삼중반복문이 처음엔 머리에 잘 안들어왔었는데, 직접 해보는게 이해에 도움이 많이 됐던 문제다.

 

코드1 (삼중 반복문)

fun main(){
    val input = readLine()!!.trim().split(" ")
    var n = input[0].toInt()
    var k = input[1].toInt()
    val dp = Array(k+1){IntArray(n+1)}

    for (i in 0..n) dp[1][i] = 1

    for (i in 2..k){		// 행: 더하는 숫자 개수
        for (j in 0..n){	// 열: 더해서 나와야 하는 값
            for (m in 0..j){
                dp[i][j] += dp[i-1][j - m] // 직전 행의 처음~현재 열까지 더한다.
                dp[i][j] %= 1_000_000_000
            }
        }
    }
    
    println(dp[k][n])
}

 

코드2 (재귀)

lateinit var dp: Array<IntArray>

fun main() {
    val input = readLine()!!.trim().split(" ")
    var n = input[0].toInt()
    var k = input[1].toInt()
    dp = Array(k+1){IntArray(n+1)}

    for (i in 0..n){
        dp[1][i] = 1	// 더하는 숫자가 하나일 때
    }

    println(findAnswer(n, k))
}

private fun findAnswer(n: Int, k:Int): Int {
    if (dp[k][n] != 0) return dp[k][n]

    for (i in 0..n){
        dp[k][n] += findAnswer(n-i, k-1)
        dp[k][n] %= 1_000_000_000
    }

    return dp[k][n]
}

 

코드3 (이중 반복문)

fun main(){
    val input = readLine()!!.trim().split(" ")
    var n = input[0].toInt()
    var k = input[1].toInt()
    val dp = Array(k+1){IntArray(n+1)}

    for (i in 1..k) dp[i][0] = 1 // 합한 값이 0인 경우는 항상 한 가지이다
    for (i in 1..n) dp[0][i] = 0 // dp[1][i] = 1로 세팅하고, 반복문을 2..k으로 시작하는 거랑 같음 

    for (i in 1..k){
        for (j in 1..n){
            dp[i][j] = dp[i][j-1] + dp[i-1][j]
            dp[i][j] %= 1_000_000_000
        }
    }

    println(dp[k][n])
}

https://www.acmicpc.net/problem/2075

 

2075번: N번째 큰 수

첫째 줄에 N(1 ≤ N ≤ 1,500)이 주어진다. 다음 N개의 줄에는 각 줄마다 N개의 수가 주어진다. 표에 적힌 수는 -10억보다 크거나 같고, 10억보다 작거나 같은 정수이다.

www.acmicpc.net

풀이

간단한 규칙이 있는 이차원 배열 수들 중 n번째로 큰 수를 찾는 문제였다.

 

공간 복잡도: 1500 *1500 * 4 byte = 9MB

시간 복잡도: 2,250,000 * log(2,250,000) = 2,250,000 * 21. ~ = 약 47,250,000

처음엔 복잡도가 충분해보였고 혹시 공간 복잡도에 문제가 있을까 싶었지만, 우선 일차원 배열을 정렬해서 답을 구해봤다. 그리고 예상외로 시간초과 판정을 받았다.

왜 시간초과가 발생했는지 찾아봤는데, 자바의 Array와 Collections의 정렬 방식이 다르기 때문이었다.
정렬의 실제 시간 복잡도는 C * 시간복잡도 + a로 알고리즘에 따라 C값이 달라지는데, Array는 퀵정렬을, Collections은 Tim 정렬이라는 걸 사용한다고 한다. 그 이유는, Array는 데이터들이 메모리에 연속적으로 존재하는 반면, list는 산발적인 연결리스트 방식으로도 존재해서 참조 지역성에 차이가 있기 때문이다. 그래서 리스트를 사용했을 때 시간 초과가 났고, IntArray로 풀어봤더니 정말로 됐다ㅋㅋ 

(자세한 내용은 https://sabarada.tistory.com/138 여기를 참고했습니다ㅋ.ㅋ)

 

 

다시 문제로 돌아와서, 고민하다가 풀이를 찾아보니 heap 또는 우선순위 큐를 사용하는 문제였다. 문제에서 주어진 배열의 크기 조건에 관계 없이 heap의 특징을 사용해서 쉽게 푸는 방법문제 조건을 함께 사용하는 방법이 있었고, 후자의 방법이 조금 더 빨랐다. 

참고) https://jihogrammer.tistory.com/25

 

 

방법1)

아이디어는 간단하다. 연산을 끝마쳤을 때 heap에 가장 큰 수 n개가 들어있게 만든다면, 그 중 최소 값이 n번째로 큰 수가 된다. 그러기 위해서, 최소heap에 수를 하나씩 넣는다. 그리고 크기를 n으로 유지하기 위해 최소 값을 삭제하고 새로운 숫자를 삽입하면, 연산할 때마다 최소 값이 하나씩 사라지므로 연산이 끝났을 때의 n개의 숫자들이 가장 큰 수의 집합이 된다. 

 

방법2)

두 번째 방법은 방법1에서 heap의 연산(logn) 횟수를 절약하는 것이다. 방법1에서는 연산을 할 때마다 heap에서 값을 빼고 넣는데, 사실 heap의 목적은 최소 값을 버림으로써 지금까지의 숫자들 중 최대 값 n개를 유지하는 것이다. 따라서, heap의 최소 값보다 작은 수는 연산할 필요가 없으므로 생략한다.

 

방법3)

세 번째는 heap과 더불어 문제의 조건도 함께 사용하는 방법이다. 다시 한번 말하지만, 핵심은 연산할 때마다가 아닌, 연산이 모두 끝났을 때 가장 큰 숫자 n개를 갖고 있는 것이다!

 

아이디어) 최대 heap을 사용한다. 처음에 heap이 전체의 최대 값을 갖고 있고, n-1번의 연산 동안 최대 값을 그 다음의 최대 값으로 교체할 수 있다면, 그때 최대heap의 값이 답이 된다.

 

이해하면 간단할 수도 있는데, 다이나믹 프로그래밍적인 느낌도 들고 처음부터 떠올리기엔 쉽지 않은 것 같다ㅋㅋ

 

구현 방법)

1. 이차원 배열에 숫자를 입력 받는다.

2. 마지막 행의 수들을 최대heap에 넣는다.

3. 그러면 heap은 처음에 가장 큰 수를 포함해서 각 열의 최대 값을 갖게 된다.

4. heap의 최대 값을 삭제한다.

5. 삭제한 값을 numbers[row][col]라고 할 때, heap에 numbers[row-1][col]을 삽입한다. 그럼 항상 모든 열의 최대 값을 heap이 갖게 되는데, 전체 수의 최대 값은 항상 모든 열의 최대 값 중 하나이다.

6. 따라서 이 연산을 n-1번 반복했을 때, 힙의 최대 값이 n번째로 큰 수가 된다.

 

구현을 위해선 이차원 배열에 값을 저장해놓고, 힙에 배열의 좌표를 함께 저장해야 한다. 

 

코드1

import java.util.*

fun main() {
    val n = readLine()!!.toInt()
    val pq = PriorityQueue<Int>() // 최소힙

    // 힙의 사이즈를 n으로 유지한다
    pq.addAll(readLine()!!.trim().split(" ").map{it.toInt()}) // 힙에 첫번째 줄 삽입
    repeat(n-1){
        val list = readLine()!!.trim().split(" ").map{ it.toInt() }
        list.forEach { number ->
            pq.offer(number)
            pq.poll()
        }
    }

    // 가장 큰 n개의 수만 남아있다
    println(pq.poll())
}

 

코드2

import java.util.*

fun main() {
    val n = readLine()!!.toInt()
    val pq = PriorityQueue<Int>(n) // 최소힙

    pq.addAll(readLine()!!.trim().split(" ").map{it.toInt()}) // 첫번째 줄 삽입
    
    repeat(n-1){
        val list = readLine()!!.trim().split(" ").map{ it.toInt() }
        list.forEach { number ->
            // 최대 값 n개를 유지한다 -> 힙의 최소 값보다 작은 수는 넣을 필요가 없다
            // 힙 연산 횟수를 줄이기 위해 힙의 최소 값보다 작은 수는 생략
            if (pq.peek() < number){	  
                pq.poll()
                pq.offer(number)
            }
        }
    }

    // 가장 큰 n개의 수만 남아있다
    println(pq.poll())
}

 

코드3

// 시간을 더 단축해보고 싶어서 StringTokenizer 사용했습니다.

import java.io.BufferedReader
import java.io.InputStreamReader
import java.util.*

data class Number(val row: Int, val column: Int, val number: Int)

fun main() = with(BufferedReader(InputStreamReader(System.`in`))) {
    val n = readLine().toInt()
    val numbers = Array(n){ mutableListOf<Number>() }

    // 이차원 배열에 좌표와 함께 저장
    repeat(n){ i ->
        val st = StringTokenizer(readLine())
        for(j in 0 until n){
            val number = st.nextToken().toInt()
            numbers[i].add(Number(i, j, number))
        }
    }

    // 최대heap
    val pq = PriorityQueue<Number>(compareByDescending{it.number}).apply{
        addAll(numbers[n-1])    // 가장 큰 수가 들어있는 마지막 라인 추가
    }
    
    // 가장 큰 수를 차례대로 n-1번 꺼낸다
    repeat(n-1){
        val maxNumber = pq.poll()
        pq.add(numbers[maxNumber.row-1][maxNumber.column]) // 그 다음으로 가장 큰 수 n개 유지
    }

    println(pq.peek().number)
}

 

+ Recent posts