Skip to main content

Command Palette

Search for a command to run...

[백준] 삼각 그래프

Updated
3 min read

Link : https://www.acmicpc.net/problem/4883

문제

이 문제는 삼각 그래프의 가장 위쪽 가운데 정점에서 가장 아래쪽 가운데 정점으로 가는 최단 경로를 찾는 문제이다.

삼각 그래프는 사이클이 없는 그래프로 N ≥ 2 개의 행과 3열로 이루어져 있다. 삼각 그래프는 보통 그래프와 다르게 간선이 아닌 정점에 비용이 있다. 어떤 경로의 비용은 그 경로에서 지나간 정점의 비용의 합이다.

오른쪽 그림은 N = 4인 삼각 그래프이고, 가장 위쪽 가운데 정점에서 가장 아래쪽 가운데 정점으로 경로 중 아래로만 가는 경로의 비용은 7+13+3+6 = 29가 된다. 삼각 그래프의 간선은 항상 오른쪽 그림과 같은 형태로 연결되어 있다.

입력

입력은 여러 개의 테스트 케이스로 이루어져 있다. 각 테스트 케이스의 첫째 줄에는 그래프의 행의 개수 N이 주어진다. (2 ≤ N ≤ 100,000) 다음 N개 줄에는 그래프의 i번째 행에 있는 정점의 비용이 순서대로 주어진다. 비용은 정수이며, 비용의 제곱은 1,000,000보다 작다.

입력의 마지막 줄에는 0이 하나 주어진다.

출력

각 테스트 케이스에 대해서, 가장 위쪽 가운데 정점에서 가장 아래쪽 가운데 정점으로 가는 최소 비용을 테스트 케이스 번호와 아래와 같은 형식으로 출력한다.

k. n

k는 테스트 케이스 번호, n은 최소 비용이다.

예제 입력 1

4
13 7 5
7 13 6
14 3 12
15 6 16
0

예제 출력 1

1. 22

1차 시도(BFS)

문제를 너무 가볍게 봤을때네느 그래프 탐색으로 풀 수 있을 것으로 생각을 했습니다. 그런데 BFS/DFS로 해결이 안되는 문제 였고 첫번때 시도는 실패로 돌아갔습니다.

import java.util.*
import kotlin.math.min

val dx = arrayListOf(1, 1, 1, 0)
val dy = arrayListOf(-1, 0, 1, 1)

fun main() {
    val br = System.`in`.bufferedReader()
    val bw = System.out.bufferedWriter()
    var cnt = 1
    while (true) {
        val n = br.readLine().toInt()

        if (n == 0) break

        val array = Array(n) { br.readLine().split(" ").map { it.toInt() }.toIntArray() }

        val weight = bfs(0 to 1, n-1 to 1, array)

        bw.write("${cnt++}. $weight")
        bw.newLine()
    }

    br.close()
    bw.close()
}

fun bfs(
    start: Pair<Int, Int>,
    end: Pair<Int, Int>,
    array: Array<IntArray>
): Int {
    var answer = Int.MAX_VALUE
    val queue = LinkedList<Node>()
    val visited = mutableSetOf<Pair<Int, Int>>()

    queue.add(Node(start.first, start.second, array[start.first][start.second]))
    visited.add(start)

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

        if (cur.x to cur.y == end) {
            answer = min(answer, cur.weight)
        }

        for (i in 0 until 4) {
            val nx = cur.x + dx[i]
            val ny = cur.y + dy[i]

            if (nx < 0 || ny < 0 || nx > array.lastIndex || ny > array[0].lastIndex) {
                continue
            }

            visited.add(nx to ny)
            queue.add(Node(nx, ny, array[nx][ny] + cur.weight))
        }
    }
    return answer
}

class Node(
    val x: Int,
    val y: Int,
    val weight: Int,
)

2차 시도(DP)

2번째로 다익스트라를 사용할까 했지만 다익스트라의 조건(가장 최선의 결과를 따라 갔을때 최적의 결과가 나온다는 보장을 할 수 있어야한다)을 만족할 수 없다는 생각을 하게 되었고 이건 DP로 푸는 것이 맞다라는 결론에 도달하게 됩니다.

그래서 점화식을 세우고 문제를 해결하게되었습니다.

점화식은 다음과 같습니다.

$$$$\begin{align} &DP[i][j] = (i, j)위치에서 가지는 가장 적은 비용 \\ &DP[i][0] = min(DP[i-1][0], DP[i-1][1]) + array[i][0] \\ &DP[i][1] = min(DP[i-1][0], DP[i-1][1], DP[i-1][2], DP[i][0]) + array[i][1] \\ &DP[i][2] = min(DP[i-1][1], DP[i-1][2], DP[i][1]) + array[i][2] \end{align}$$

$$

점화식이 복잡하다고 생각이되지만 삼각 그래프의 특성으로 인해서 같은 행, 열일때와 아닐때는 구분해서 점화식에 고려를 해줘야하기 때문에 복잡할 수 밖에 없다고 생각이 됩니다.

결론 적으로 코드로 적으면 아래와 같습니다.

fun main() {
    val br = System.`in`.bufferedReader()
    val bw = System.out.bufferedWriter()
    var cnt = 1

    while (true) {
        val n = br.readLine().toInt()

        if (n == 0) break

        val array = Array(n) { br.readLine().split(" ").map { it.toInt() }.toIntArray() }
        val dp = Array(n) { IntArray(3) { Int.MAX_VALUE } }

        // init
        dp[0][1] = array[0][1]
        dp[0][2] = dp[0][1] + array[0][2]

        for (i in 1 until n) {
            dp[i][0] = minOf(dp[i - 1][0], dp[i - 1][1]) + array[i][0]
            dp[i][1] = minOf(dp[i - 1][0], dp[i - 1][1], dp[i - 1][2], dp[i][0]) + array[i][1]
            dp[i][2] = minOf(dp[i - 1][1], dp[i - 1][2], dp[i][1]) + array[i][2]
        }

        bw.write("${cnt++}. ${dp[n - 1][1]}")
        bw.newLine()
    }

    br.close()
    bw.close()
}

여기서 주의할 점, 처음 row를 초기화를 잘 해야합니다.

        dp[0][1] = array[0][1]
        dp[0][2] = dp[0][1] + array[0][2]

(0, 1)에서 동작하기 때문에 (0, 1),(0, 2)에 대해서 초기화를 직접 해줘야하는 것을 고려해야합니다.

More from this blog

JVM은 컨테이너의 CPU와 메모리 한계를 어떻게 알아낼까

8코어 노드에 컨테이너를 띄웠는데 ForkJoinPool이 스레드를 한두 개만 만들어요. 메모리는 넉넉히 줬는데 컨테이너가 자꾸 OOMKilled로 죽고요. 분명히 같은 JAR인데 로컬에서는 멀쩡하다가 쿠버네티스에만 올리면 이상해져요. 이 글은 "왜 컨테이너 속 JVM은 다르게 행동하는가"를 cgroup이라는 진짜 경계선과, JVM이 그 경계를 읽어내는 내

May 21, 202615 min read

ThreadPoolExecutor는 언제 스레드를 새로 만들까 — execute()의 3단계

Executors.newFixedThreadPool(10) 한 줄을 쓰면서도, 11번째 작업이 오면 스레드가 11개로 늘어날 거라고 막연히 기대해 본 적 없으신가요. 실제로는 큐가 먼저 무한히 쌓이고 스레드는 영원히 10개에 머물러요. 이 글은 ThreadPoolExecutor가 작업을 받았을 때 "스레드를 새로 만들지, 큐에 넣을지, 거부할지"를 결정하는

May 21, 202617 min read

자바 synchronized는 어떻게 동작할까 — 모니터, 락 인플레이션, 그리고 사라진 biased locking

synchronized 키워드 하나로 스레드 안전을 얻는 동안, JVM 안에서는 객체 헤더의 비트를 뒤집고, 스택에 락 레코드를 쌓고, 경합이 생기면 네이티브 모니터로 승격하는 일이 벌어져요. 이 글은 그 한 번의 잠금이 객체 헤더부터 ObjectMonitor까지 어떤 경로를 거치는지, 그리고 한때 있었다가 JDK 18에서 사라진 biased locking

May 19, 202616 min read

JVM 객체 할당의 비밀 — TLAB, Bump-the-Pointer, 그리고 할당이 거의 공짜인 이유

Java에서 new를 호출하면 무슨 일이 벌어질까요? "힙에 메모리를 잡는다"는 한 문장 뒤에는 스레드마다 자기만의 분양 구역을 나눠 갖는 정교한 설계가 숨어 있어요. 이 글은 HotSpot JVM이 객체 할당을 어떻게 "거의 공짜"로 만드는지 그 내부를 따라가 보려는 글이에요. JVM 메모리 동작 원리에 관심 있는 분께 권해요. 자바를 쓰다 보면 객체를

May 15, 202614 min read

Java Zero-Copy — FileChannel.transferTo, sendfile, 그리고 Kafka가 디스크를 네트워크로 흘려보내는 방법

"파일을 읽어서 소켓으로 보낸다." 한 줄짜리 요구사항이에요. 그런데 이 한 줄 뒤에서 데이터는 메모리를 네 번이나 복사하고, CPU는 커널과 유저 공간을 네 번이나 들락거려요. Kafka처럼 초당 수십만 건을 흘려보내야 하는 시스템에서 이 비용은 그냥 넘길 수가 없어요. 이 글은 그 복사를 한 겹씩 벗겨내는 zero-copy의 동작 원리를 따라가요. 전통

May 15, 202617 min read

끄적끄적 테크 블로그

165 posts

물류 회사에 다니고 있는 개발자 블로그입니다. 개발을 너무 좋아해서 정신없이 작업하다가 중간에 끄적거리며 내용들을 몇개 적어봅니다 ㅎㅎ