본문 바로가기

알고리즘

백준 - 18500번: 미네랄(Kotlin)

제한사항

첫째 줄에 동굴의 크기 R과 C가 주어진다. (1 ≤ R,C ≤ 100)

다음 R개 줄에는 C개의 문자가 주어지며, '.'는 빈 칸, 'x'는 미네랄을 나타낸다.

다음 줄에는 막대를 던진 횟수 N이 주어진다. (1 ≤ N ≤ 100)

마지막 줄에는 막대를 던진 높이가 주어지며, 공백으로 구분되어져 있다. 모든 높이는 1과 R사이이며, 높이 1은 행렬의 가장 바닥, R은 가장 위를 의미한다. 첫 번째 막대는 왼쪽에서 오른쪽으로 던졌으며, 두 번째는 오른쪽에서 왼쪽으로, 이와 같은 식으로 번갈아가며 던진다.

느낀 점

아이디어 생각하는데 은근 시간을 뺏겼고, 구현하는 데에도 시간을 많이 뺏겼다.

요즘 느끼는 건데, 알고리즘에 큰 틀만 짜고 바로 코드 작성에 들어가면 이번처럼 코드가 꼬이거나 망가진다.

가독성이 떨어지고, 방향을 반대로 처리하거나, 불필요한 코드가 반복된다.

문제 자체가 난이도가 높아서 어려운 것도 있었지만, 처음 시작할 때 큰 틀만이 아니라 디테일을 생각하고 갔어야 했다.

다음에는 반성하고 그렇게 풀자.

그래도 예전에 브론즈에서 쩔쩔 매던 시절 생각하면 많이 발전했다.

코드

import java.io.BufferedReader
import java.io.InputStreamReader
import java.util.LinkedList
import kotlin.math.absoluteValue

// 위부터 시계방향
private val dy = intArrayOf(1, 0, -1, 0)
private val dx = intArrayOf(0, 1, 0, -1)

fun main() = with(BufferedReader(InputStreamReader(System.`in`))) {
    val (r, c) = readLine().split(" ").map { it.toInt() }
    var map = Array(r) { readLine().toCharArray() }
    reverseArray(map)

    var stickCnt = readLine().toInt()
    val stickThrownHeights = readLine().split(" ").map { it.toInt() - 1 }.toIntArray()

    for (stickIdx in stickThrownHeights.indices) {
        val height = stickThrownHeights[stickIdx]
        val thrownStickCount = stickIdx + 1
        val direction = if (thrownStickCount.rem(2) == 1) {
            Direction.LEFT
        } else {
            Direction.RIGHT
        }

        val crashingCol = throwStick(height, direction, map)
        // 던진 막대가 아무 것도 붙이지 않았으면 다음 막대기로 넘어간다.
        if (crashingCol == Int.MAX_VALUE) {
            continue
        }
        
        // 던진 부분 주변 클러스터 확인
        val crashingAroundMineral = LinkedList<Pair<Int, Int>>()
        for (directionIdx in 0..3) {
            val nextY = height + dy[directionIdx]
            val nextX = crashingCol + dx[directionIdx]
            if (nextY !in map.indices || nextX !in map[0].indices) {
                continue
            }

            // 막대기가 부딪힌 곳 주변에 미네랄이 있으면 부셔졌는지 확인하기 위해 리스트에 넣어준다.
            if (map[nextY][nextX] == 'x') {
                crashingAroundMineral.push(nextY to nextX)
            }
        }

        // 막대기가 부딪힌 곳 주변에 미네랄이 없으면, 미네랄이 떨어질 일이 없으므로 다음 막대기로 넘어간다.
        if (crashingAroundMineral.first == null) {
            continue
        }

        // BFS를 통해, 클러스터를 구하는 과정
        val visited = Array(r) { BooleanArray(c) { false } }
        val clusterList = LinkedList<LinkedList<Pair<Int, Int>>>()
        for (mineralPosition in crashingAroundMineral) {
            if (visited[mineralPosition.first][mineralPosition.second]) {
                continue
            }

            val queue = LinkedList<Pair<Int, Int>>()
            queue.offer(mineralPosition)
            visited[mineralPosition.first][mineralPosition.second] = true
            // 클러스터의 미네랄 좌표들을 저장할 리스트
            val cluster = LinkedList<Pair<Int, Int>>()
            cluster.push(mineralPosition)

            while (queue.isNotEmpty()) {
                val searchedMineral = queue.poll()

                for (directionIdx in 0..3) {
                    val nextY = searchedMineral.first + dy[directionIdx]
                    val nextX = searchedMineral.second + dx[directionIdx]

                    if (nextY !in map.indices || nextX !in map[0].indices || visited[nextY][nextX]) {
                        continue
                    }
                    visited[nextY][nextX] = true

                    if (map[nextY][nextX] != 'x') {
                        continue
                    }

                    // 탐색하지 않았으므로 BFS를 위해 queue에 넣는다.
                    queue.offer(nextY to nextX)
                    // BFS를 통해 인접한 미네랄을 구한 것이므로 같은 클러스터에 속한다.
                    // 따라서, 클러스터에 넣어준다.
                    cluster.push(nextY to nextX)
                }
            }

            // 클러스터를 구했으므로 클러스터 리스트에 넣어준다.
            clusterList.add(cluster)
        }

        // 해당 클러스트가 내려갈 수 있는지 확인
        for (cluster in clusterList) {
            // 내려 갈 때, 미네랄을 만나면 클러스터에 속한 미네랄인지 다른 클러스터에 속한 미네랄인지 확인이 불가능하다.
            // 따라서, 현재 클러스터가 제거된 맵을 만든다. 이렇게하면 맵에서 만난 미네랄은 모두 자신이 아닌 다른 클러스터에 속한 미네랄이다.
            val clusterRemovedMap = Array(r) { CharArray(c) }
            copyArray(map, clusterRemovedMap)
            cluster.forEach {
                clusterRemovedMap[it.first][it.second] = '.'
            }

            // 밑으로 이동 가능한 횟수를 구한다.
            val movement = getMaximumMovement(cluster, clusterRemovedMap)
            // 0이면 이동 필요 없음. 다음 클러스터를 확인한다.
            if (movement == 0) {
                continue
            }

            // 클러스터가 하나라도 움직이면, 다른 클러스터는 체크할 필요가 없다.
            // 문제 조건에서 하나만 움직인다고 조건을 줬으니까.
            var isClusterMoved: Boolean = false
            for (pair in cluster) {
                clusterRemovedMap[pair.first - movement][pair.second] = 'x'
                isClusterMoved = true
            }

            // 클러스트는 하나만 움직일 수 있으므로, 움직인 순간 다른 클러스터 체크할 필요가 없음.
            if (isClusterMoved) {
                map = clusterRemovedMap
                break
            }
        }
    }

    reverseArray(map)
    map.forEach {
        println(it)
    }
}

fun getMaximumMovement(cluster: LinkedList<Pair<Int, Int>>, clusterRemovedMap: Array<CharArray>): Int {
    var movement = 1
    while (movement.absoluteValue <= clusterRemovedMap.size) {
        var isLimitDownMovement = false
        for (mineral in cluster) {
            val nextY = mineral.first - movement
            val nextX = mineral.second
            // 맵 밖이거나, 밑에 미네랄이 있으면 더 못내려감.
            if (nextY !in clusterRemovedMap.indices || nextX !in clusterRemovedMap[0].indices || clusterRemovedMap[nextY][nextX] == 'x') {
                isLimitDownMovement = true
                break
            }
        }

        if (!isLimitDownMovement) {
            movement++
            continue
        }
        return --movement
    }

    return -1
}

enum class Direction {
    LEFT, RIGHT
}

fun throwStick(height: Int, startDirection: Direction, map: Array<CharArray>): Int {
    if (startDirection == Direction.LEFT) {
        for (col in map[0].indices) {
            if (map[height][col] == 'x') {
                map[height][col] = '.'
                return col
            }
        }
    } else {
        for (col in map[0].size - 1 downTo 0) {
            if (map[height][col] == 'x') {
                map[height][col] = '.'
                return col
            }
        }
    }

    return Int.MAX_VALUE
}

fun copyArray(original: Array<CharArray>, destination: Array<CharArray>) {
    for (r in original.indices) {
        for (c in original[0].indices) {
            destination[r][c] = original[r][c]
        }
    }
}

fun reverseArray(array: Array<CharArray>) {
    for (col in array[0].indices) {
        for (row in 0..<(array.size/2)) {
            val temp = array[row][col]
            array[row][col] = array[array.size - row - 1][col]
            array[array.size - row - 1][col] = temp
        }
    }
}
728x90
반응형

'알고리즘' 카테고리의 다른 글

백준 - 구슬 탈출 2(Kotlin)  (1) 2023.12.28
1309번: 동물원(Kotlin, C++)  (0) 2023.12.10
70. Climbing Stairs  (0) 2023.07.25
2349. Design a Number Container System  (0) 2023.07.25
프로그래머스 - 미로 탈출(Kotlin)  (0) 2023.03.13