문제 풀이 ✏️/탐색ㆍ그래프

[백준] 26169 (세 번 이내에 사과를 먹자)

interfacer_han 2025. 4. 8. 15:54

#1 알고리즘

 

그래프 - 깊이 우선 탐색 (DFS, Depth-First Search)

#1 그래프의 탐색 깊이 우선 탐색 - 위키백과, 우리 모두의 백과사전위키백과, 우리 모두의 백과사전. 깊이 우선 탐색 깊이 우선 탐색의 애니메이션 예시 깊이 우선 탐색( - 優先探索, 영어: depth-f

kenel.tistory.com

DFS를 이용했다. 한 단계식 정복해가며 차근차근 나아가는 너비 우선 탐색과 달리, 깊이 우선 탐색은 '한 방향으로 갈 때까지 가보기'를 반복한다. 따라서 BFS보단 DFS가 이 문제에 어울린다. 이 문제에서는 최대 3번 이동해야 한다는 제한이 있다. 깊이 우선 탐색 알고리즘을 살짝 고쳐서, 문제에서 제시한 깊이까지만 수행되도록 만들었다.

 

#2 코드 - 코틀린

#2-1 성공한 코드

var yRange = 0..4 // i
var xRange = 0..4 // j
var depthRange = 0..3
val box = Array(5) { Array(5) { -1 } }
val visited = Array(5) { Array(5) { false } }
var r = -1
var c = -1

const val APPLE = 1
const val EMPTY = 0
const val WALL = -1

const val RESULT_CAN_EAT_APPLE_TWO_OR_MORE = 1
const val RESULT_CANT_EAT_APPLE_TWO_OR_MORE = 0

var result = RESULT_CANT_EAT_APPLE_TWO_OR_MORE

fun main() {
    // (1) 입력 받기
    for (y: Int in yRange) {
        val row = readln().split(" ")
        for (x: Int in xRange) {
            box[y][x] = row[x].toInt()
        }
    }
    val rAndC = readln().split(" ")
    r = rAndC[0].toInt()
    c = rAndC[1].toInt()

    // (2) dfs
    dfs(r, c, visited, 0, 0)  // depth를 0-based indexing으로 둔다. 즉, 3이 depth의 최댓값이 된다.

    // (3) 출력
    println(
        if (result == RESULT_CAN_EAT_APPLE_TWO_OR_MORE) {
            RESULT_CAN_EAT_APPLE_TWO_OR_MORE
        } else {
            RESULT_CANT_EAT_APPLE_TWO_OR_MORE
        }
    )

}

fun dfs(y: Int, x: Int, visited: Array<Array<Boolean>>, depth: Int, prevAppleCount: Int) {
    visited[y][x] = true
    val appleCount = if (box[y][x] == APPLE) {
        prevAppleCount + 1
    } else {
        prevAppleCount
    }

    if (2 <= appleCount) {
        result = RESULT_CAN_EAT_APPLE_TWO_OR_MORE
        return
    }

    for (next in getNexts(y, x)) {
        val nextY = next.first
        val nextX = next.second
        val nextDepth = depth + 1

        if (!visited[nextY][nextX] && nextDepth in depthRange) {
            dfs(nextY, nextX, visited, nextDepth, appleCount)
        }
    }
    
    visited[y][x] = false
}

fun getNexts(prevY: Int, prevX: Int): List<Pair<Int, Int>> {
    val candidates = listOf(
        Pair(prevY - 1, prevX),
        Pair(prevY + 1, prevX),
        Pair(prevY, prevX - 1),
        Pair(prevY, prevX + 1),
    )
    val nexts = ArrayList<Pair<Int, Int>>()
    for (candidate in candidates) {
        val candidateY = candidate.first
        val candidateX = candidate.second

        if (candidateY in yRange && candidateX in xRange && box[candidateY][candidateX] != WALL) {
            nexts.add(candidate)
        }
    }

    return nexts
}

실패했던 코드(#2-2)에서 딱 한 줄 visited[y][x] = false 추가한 코드다. 이 코드는 백트래킹 코드다. 백트래킹에 대한 설명은 여기를 참조하자.

 

#2-2 실패했던 코드

var yRange = 0..4 // i
var xRange = 0..4 // j
var depthRange = 0..3
val box = Array(5) { Array(5) { -1 } }
val visited = Array(5) { Array(5) { false } }
var r = -1
var c = -1

const val APPLE = 1
const val EMPTY = 0
const val WALL = -1

const val RESULT_CAN_EAT_APPLE_TWO_OR_MORE = 1
const val RESULT_CANT_EAT_APPLE_TWO_OR_MORE = 0

var result = RESULT_CANT_EAT_APPLE_TWO_OR_MORE

fun main() {
    // (1) 입력 받기
    for (y: Int in yRange) {
        val row = readln().split(" ")
        for (x: Int in xRange) {
            box[y][x] = row[x].toInt()
        }
    }
    val rAndC = readln().split(" ")
    r = rAndC[0].toInt()
    c = rAndC[1].toInt()

    // (2) dfs
    dfs(r, c, visited, 0, 0)  // depth를 0-based indexing으로 둔다. 즉, 3이 depth의 최댓값이 된다.

    // (3) 출력
    println(result)

}

fun dfs(y: Int, x: Int, visited: Array<Array<Boolean>>, depth: Int, prevAppleCount: Int) {
    visited[y][x] = true
    val appleCount = if (box[y][x] == APPLE) {
        prevAppleCount + 1
    } else {
        prevAppleCount
    }

    if (2 <= appleCount) {
        result = RESULT_CAN_EAT_APPLE_TWO_OR_MORE
        return
    }

    for (next in getNexts(y, x)) {
        val nextY = next.first
        val nextX = next.second
        val nextDepth = depth + 1

        if (!visited[nextY][nextX] && nextDepth in depthRange) {
            dfs(nextY, nextX, visited, nextDepth, appleCount)
        }
    }
}

fun getNexts(prevY: Int, prevX: Int): List<Pair<Int, Int>> {
    val candidates = listOf(
        Pair(prevY - 1, prevX),
        Pair(prevY + 1, prevX),
        Pair(prevY, prevX - 1),
        Pair(prevY, prevX + 1),
    )
    val nexts = ArrayList<Pair<Int, Int>>()
    for (candidate in candidates) {
        val candidateY = candidate.first
        val candidateX = candidate.second

        if (candidateY in yRange && candidateX in xRange && box[candidateY][candidateX] != WALL) {
            nexts.add(candidate)
        }
    }

    return nexts
}

실패한 이유는 코드 속 visited를 BFS처럼 다뤘기 때문이다.

 

A는 출발점 B는 도착점들의 모임이다. 검은색 칸은 장애물이다. A는 문제마다 고정된 값(r과 c)이지만 B는 3번 이동할 수 있는 모든 칸들의 모임이다. 위 도식도에서 b'는 그 모임의 한 원소를 나타낸 것이다. 파란색 칸에 주목하라. 파란색 칸은 이동과 동시에 '방문함'으로 처리될 것이다.

 

파란색 칸의 존재 때문에, b''는 영원히 방문할 수 없게 된다. 즉 DFS를 수행할 때는 방문 기록을 '초기화'해야 한다. BFS때는 한 단계 (한 깊이)가 마무리되어야 다음 단계로 넘어가기에 초기화할 일이 없었던 것과 대조적이다 (물론 BFS도 문제의 조건이 어떻게 나오느냐 따라 필요할 수도 있겠지만 말이다).