Uknow's Lab.
article thumbnail

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

 

17472번: 다리 만들기 2

첫째 줄에 지도의 세로 크기 N과 가로 크기 M이 주어진다. 둘째 줄부터 N개의 줄에 지도의 정보가 주어진다. 각 줄은 M개의 수로 이루어져 있으며, 수는 0 또는 1이다. 0은 바다, 1은 땅을 의미한다.

www.acmicpc.net

 

난이도 : 골드 1
태그 : 구현, 그래프 이론, 브루트포스 알고리즘, 그래프 탐색, 너비 우선 탐색, 깊이 우선 탐색, 최소 스패닝 트리

 

 

1. 설명

DFS / BFS를 사용해 각 섬을 그룹화 해줍니다.

그리고 각 섬의 모든 점들을 저장해 놨다가, 상하좌우로 방향으로 다리를 만들 수 있는지 확인합니다.

상하좌우 방향으로 현재의 섬을 만난다면 다리를 만들 수 없으며, 다른 섬을 만나게 되면 다리를 만들 수 있습니다.

이 다리를 간선으로 취급해 큐에 넣습니다.

 

모든 섬들을 다리로 이어으면서, 다리의 총 길이가 최소가 되어야 하는데,

그룹화된 각 섬들(Node, Vertex)

그리고 그룹화된 각 섬을 이어주는 다리(Edge)

최소 스패닝 트리를 적용하기 아주 좋은 형태가 되었습니다.

 

 

 

2. 소스코드

<kotlin />
import java.util.PriorityQueue import java.util.StringTokenizer data class Point(val x: Int, val y: Int) // x, y 좌표 저장용 클래스 // 시작 노드, 도착 노드, 간선의 비용 data class Edge(val from: Int, val to: Int, val weight: Int) : Comparable<Edge> { override fun compareTo(other: Edge): Int { return this.weight - other.weight // 간선 기준으로 정렬 } } lateinit var map: Array<Array<Int>> lateinit var parent: Array<Int> // 분리 집합에 쓸 부모 저장 배열 var islandIndex = 0 // 각 섬을 그룹화하는데 쓸 각 섬의 index var n = 0 var m = 0 val islandList = ArrayList<Point>() // 섬들의 모든 좌표 val dx = arrayOf(1, -1, 0, 0) val dy = arrayOf(0, 0, 1, -1) fun main() = with(System.`in`.bufferedReader()) { var st = StringTokenizer(readLine()) n = st.nextToken().toInt() m = st.nextToken().toInt() // 바다 0, 섬 -1로 초기화 map = Array(n) { st = StringTokenizer(readLine()) Array(m) { if (st.nextToken().toInt() == 0) 0 else -1 } } // 섬을 그룹화함 repeat(n) { x -> repeat(m) { y -> if (map[x][y] == -1) { islandIndex++ groupingIsland(x, y) } } } parent = Array(islandIndex + 1) { it } val pq = PriorityQueue<Edge>() // 섬에서 다른 섬으로 가는 모든 다리를 탐색 (간선을 만드는 과정) islandList.forEach { point -> for (i in 0 until 4) { var bridgeLength = 1 while (true) { val nx = point.x + dx[i] * bridgeLength val ny = point.y + dy[i] * bridgeLength // 이동헀는데 현재 섬임 -> 다음 케이스로 if (nx !in 0 until n || ny !in 0 until m || map[nx][ny] == map[point.x][point.y]) break // 현재 섬이 아니면서, 바다가 아님 => 다른 섬을 만남 if (map[nx][ny] != 0) { if (bridgeLength >= 3) { // 근데 다리 길이가 2 이상 되야함 pq.offer(Edge(map[point.x][point.y], map[nx][ny], bridgeLength - 1)) // 시작 섬, 도착 섬, 다리 길이 } break } bridgeLength++ } } } var total = 0 var bridgeCnt = 0 // 최소 스패닝 트리 알고리즘 적용 while (pq.isNotEmpty()) { val target = pq.poll() if (union(target.from, target.to)) { total += target.weight // 가중치의 합 bridgeCnt++ // 다리의 개수 } } // 다리가 (섬 - 1) 개 -> 모든 섬을 이음! // 다리가 (섬 - 1) 개 보다 적음 -> 모든 섬을 이을 수 없음. -1 출력 println(if (bridgeCnt == islandIndex - 1) total else -1) } // dfs를 사용해 각 섬을 그룹화함 fun groupingIsland(x: Int, y: Int) { map[x][y] = islandIndex islandList.add(Point(x, y)) // 섬의 모든 좌표를 리스트에 저장 for (i in 0 until 4) { val nx = x + dx[i] val ny = y + dy[i] if (nx !in 0 until n || ny !in 0 until m || map[nx][ny] != -1) continue groupingIsland(nx, ny) } } fun find(x: Int): Int { return if (x == parent[x]) x else { parent[x] = find(parent[x]) parent[x] } } fun union(x: Int, y: Int): Boolean { val nx = find(x) val ny = find(y) return if (nx != ny) { parent[nx] = ny true } else false }

 

 

dfs 혹은 bfs를 사용해 각 섬을 그룹화 한 뒤,

섬의 모든 정점에서 다른 섬으로 가는 경로를 탐색하여 (다리가  중간에 꺾이면 안되므로 그냥 십자 방향으로만 탐색)

노드와 엣지를 구하는게 이번 문제의 포인트라 할 수 있습니다.

 

 

3. 후기

굉장히 재밌게 풀었던 문제였습니다.

각 섬을 그룹화하고, 최소 스패닝 트리를 적용시켜야겠다는 생각은 했는데

간선을 어떻게 해야하나 고민하다가 질문게시판을 보고

섬의 모든 좌표에서 십자 모양으로 탐색해 나간 뒤 다른 섬을 만나면 다리를 만들 수 있다는 것이 꽤 인상적이였습니다.

profile

Uknow's Lab.

@유노 Uknow

인생은 Byte와 Double 사이 Char다. 아무말이나 해봤습니다.