티스토리 뷰

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

 

17472번: 다리 만들기 2

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

www.acmicpc.net

삼성 역량테스트 A형 (= 삼성 SW직군 공채 코딩테스트 수준) 기출 문제.

 

위 그림과 같은 맵이 주어지고, 파란색을 섬이라고 한다. 모든 섬을 연결할 수 있도록 다리 (회색) 를 놓아야 하는데 다리의 총 길이가 최소가 되도록 놓았을 때 그 길이를 구하는 문제이다.

 

> 다리는 가로 or 세로, 일직선으로만 놓아야 한다

> 두 섬을 잇기 위해 다리를 놓았는데 중간에 또다른 섬이랑 인접하더라도 그것은 연결된 것이 아니다 (그림의 D)

> 다리의 길이는 최소 2여야 한다 (길이 1짜리 다리는 놓을 수 없다)

> 다리가 교차할 수도 있으나 그 경우 교차하는 칸은 각 다리에서 모두 길이에 포함된다

 

입력은 아래와 같이 맵의 가로, 세로 길이와 맵의 상태 (0: 바다, 1: 육지) 가 주어진다.

7 8
0 0 0 0 0 0 1 1
1 1 0 0 0 0 1 1
1 1 0 0 0 0 0 0
1 1 0 0 0 1 1 0
0 0 0 0 0 1 1 0
0 0 0 0 0 0 0 0
1 1 1 1 1 1 1 1

- 풀이:

각 섬을 정점 (vertex), 다리를 간선 (edge) 이라 생각하면 그래프로 볼 수 있으며, 모든 정점을 연결하면서 간선 비용의 합이 최소가 되는 것을 구하는 것이므로 MST (최소비용 신장트리) 문제이다.

다만 입력이 그래프 표현 형태로 주어지지 않으므로 주어지는 맵을 그래프로 변환하기 위한 전처리 작업이 필요하다.

 

1) 각 섬을 구별하기 위해 섬에 번호를 붙인다. - BFS/DFS

유명한 BFS/DFS 문제인 BOJ 2667 단지번호붙이기와 동일한 방법으로 하면 된다.

-> 각 섬 = 그래프의 정점이라 생각한다.

 

2) 모든 육지인 칸에 대하여, 바다를 향해 다리를 놓아보고 다른 섬에 닿는지 조사하고, 닿는 경우 그 길이를 저장한다. - 사방탐색, 완전탐색

-> 다리의 길이 = 섬과 섬을 잇는 간선의 길이라 생각한다.

 

3) 만들어진 정점과 간선 정보를 이용해 MST 알고리즘을 돌려 모든 섬을 잇는 최소비용을 구한다. - MST (프림/크루스칼 알고리즘)

 

- 코드 (Java):

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.LinkedList;
import java.util.PriorityQueue;
import java.util.Queue;
import java.util.StringTokenizer;

// 간선 정보를 Priority Queue에 저장하기 위한 Edge 클래스
class Edge implements Comparable<Edge>{
	int start;
	int end;
	int cost;
	
	public Edge(int start, int end, int cost) {
		this.start = start;
		this.end = end;
		this.cost = cost;
	}

	@Override
	public int compareTo(Edge o) { // Priority Queue에서의 정렬기준 = cost
		return Integer.compare(this.cost, o.cost);
	}
}

public class boj_17472 {
	
	static int N, M;
	static int[][] map;
	static int[] dx = {0, 1, 0, -1};
	static int[] dy = {-1, 0, 1, 0};
	static PriorityQueue<Edge> pq;
	static int[] uf_parents;
	
	public static void main(String[] args) throws IOException {
		BufferedReader bf = new BufferedReader(new InputStreamReader(System.in));
		StringTokenizer st = new StringTokenizer(bf.readLine(), " ");
		
		N = Integer.parseInt(st.nextToken());
		M = Integer.parseInt(st.nextToken());
		map = new int[N][M];
		pq = new PriorityQueue<Edge>();
		
        // 크루스칼에서 사용하는 union-find 알고리즘을 위한 배열
		uf_parents = new int[8]; // 섬 최대 6개 -> 번호 최대 7번까지 가능

		for (int i=0; i<N; i++) {
			String line = bf.readLine();
			for (int j=0; j<M; j++) {
				map[i][j] = line.charAt(j*2) - '0';
			}
		} // 여기까지 입력
		
		// 섬번호 붙이기
		int island_num = 2; // 1은 이미 map에서 사용하고 있으므로 혼동 방지를 위해 2번부터
		for (int i=0; i<N; i++) {
			for (int j=0; j<M; j++) {
				if (map[i][j] == 1) {
					uf_parents[island_num] = -1;
					bfs(j, i, island_num++);
				}
			}
		}
		
		// 섬에 해당하는 모든 칸에서 가로나 세로로 이동해보면서 각 섬간의 거리 구하기
		for (int i=0; i<N; i++) {
			for (int j=0; j<M; j++) {
				if (map[i][j] != 0) {
					// 사방탐색
					for (int d=0; d<4; d++) {
						int nx = j + dx[d];
						int ny = i + dy[d];
                        // 다음 칸이 바다인 경우에만 다리 놓아보기 시도
						if (nx >= 0 && ny >= 0 && nx < M && ny < N && map[ny][nx] == 0) {
							getDist(j, i, map[i][j], dx[d], dy[d]);
						}
					}
					
				}
			}

		} 
		
		// 크루스칼 알고리즘
		int edgecount = 0;
		int ans = 0;
		boolean flag;
		while (edgecount < island_num-3) { // 섬개수 -1개의 edge 선택 (섬번호 2부터 시작하므로 -3)
			if (pq.isEmpty())
				break;
			
			Edge curEdge = pq.poll();
			flag = union(curEdge.start, curEdge.end); // 사이클 체크
			if (flag) {
				edgecount++;
				ans += curEdge.cost;
			}
		}
		
		// 섬개수 -1개의 edge를 선택하지 못하고 중간에 break 된 경우 모든 섬을 연결할 수 없다는 뜻
		if (edgecount < island_num-3)
			System.out.println(-1);
		else
			System.out.println(ans);
		
	}
	
    // 크루스칼 알고리즘에서 사이클 체크를 위한 union-find 알고리즘 파트
	private static boolean union(int a, int b) {
		int a_root = find_set(a);
		int b_root = find_set(b);
		if (a_root != b_root) {
			uf_parents[b_root] = a;
			return true;
		}
		else {
			return false;
		}
	}

	private static int find_set(int a) {
		if (uf_parents[a] == -1) {
			return a;
		}
		else {
			return uf_parents[a] = find_set(uf_parents[a]);
		}
	}

	// 다리 놓아보는 부분
	private static void getDist(int x, int y, int num, int dx, int dy) {
		
		int dist = 0;
		x += dx;
		y += dy;
		dist++;
		
		while (x >= 0 && y >= 0 && x < M && y < N) {
			if (map[y][x] != 0) { // 가로나 세로로 이동하다가, 다른 섬을 찾으면...
				if (dist > 2) { // dist=2는 길이 1짜리 다리인 경우임
					pq.add(new Edge(num, map[y][x], dist-1));
				}
				break;
			}
			
			x += dx;
			y += dy;
			dist++;
		}
		
	}

	// 섬 번호 붙이는 BFS
    // 맵에 번호가 새로 붙는 것 자체가 visited 표시. 따로 visited 배열은 필요없다
	private static void bfs(int x, int y, int num) {
		Queue<int[]> q = new LinkedList<int[]>();
		q.offer(new int[] {x, y});
		
		while(!q.isEmpty()) {
			int[] p = q.poll();
			map[p[1]][p[0]] = num;

			for (int i=0; i<4; i++) {
				int nx = p[0] + dx[i];
				int ny = p[1] + dy[i];
						
				if (nx >= 0 && ny >= 0 && nx < M && ny < N && map[ny][nx] == 1) {
					q.offer(new int[] {nx, ny});
				}
			}
		}
	}
}

 

주의점:

1) 파트 - map에 이미 0, 1이 저장되어 있으므로 새로운 섬 번호는 2번부터 붙여야 한다는 점. (입력을 받을 때 1이 나올 때마다 1 말고 다른 번호로 바꿔버리는 방법도 있긴 하다)

2) 파트 - 길이 1짜리 다리는 허용되지 않는다는 점.

3) 파트 - (섬 개수 - 1) 개 만큼의 edge가 만들어지지 않는 경우가 있다는 점. -1을 출력하는 경우이다. 에러가 발생하지 않도록 PQ가 비는 경우엔 루프를 탈출시켜줘야 한다.

 

+ 2) 파트에서 같은 정점 쌍을 잇는 edge가 중복으로 PQ에 들어갈 수 있으나, union-find에서 걸러지기 때문에 별로 상관없다.

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함