본문 바로가기
Algorithm

[백준] 할로윈 묘지(3860) Java

by Kloong 2021. 8. 11.
 

3860번: 할로윈 묘지

오늘은 할로윈이다. 상근이와 친구들은 할로윈을 기념하기 위해 묘지를 방문했다. 상근이와 친구들은 한 명씩 묘지로 들어가고, 혼자서 묘지의 출구를 찾아야 한다. 이제, 상근이의 차례가 돌아

www.acmicpc.net

 

문제 요약

더보기

할로윈에 할짓이 없는지 묘지 탐험을 하는 상근이. 입구로 들어가서 출구로 최대한 빨리 나오는 것이 목표.

묘지

묘지는 W × H 크기의 그리드 형태. 묘지의 입구는 (0, 0)이고, 출구는 (W-1, H-1)이다.

상근이는 최대한 빨리 묘지를 나가려고 한다. 그리고 상근이는 이동하던 도중 출구에 도달하면 그 즉시 묘지를 빠져나간다.

상근이는 현재 있는 칸과 동, 서, 남, 북으로 인접한 칸으로 이동할 수 있다.

각 칸은 잔디, 묘비, 또는 귀신 구멍이다.

  • 묘비는 매우 높기 때문에, 묘비가 있는 칸으로는 이동할 수 없다.
  • 귀신 구멍이 있는 칸으로 이동하면, 특정 시간이 지난 후에 묘지의 다른 곳에서 상근이가 나타나게 된다. 시간은 귀신 구멍마다 다르며, 양수, 음수, 0 중 하나이다.
  • 잔디가 있는 칸으로는 자유롭게 이동할 수 있다. 인접한 칸으로 이동할 때 걸리는 시간은 1이다.

상근이는 묘지를 빨리 나가기 위해 귀신 구멍도 이용할 것이다. 묘지를 나갈 수 없는 경우나, 계속해서 과거로 이동하는 경우가 존재할 수도 있다.

 

입력

더보기

여러 개의 테스트 케이스로 이루어져 있다.

 

각 테스트 케이스의 첫째 줄에는 묘지의 너비 W와 높이 H가 주어진다. (1 ≤ W, H ≤ 30)

 

다음 줄에는 묘비의 개수 G (G ≥ 0)가 주어진다.

다음 G개 줄에는 묘비의 위치를 나타내는 두 정수 X와 Y가 주어진다. (0 ≤ X < W, 0 ≤ Y < H)

 

다음 줄에는 귀신 구멍의 수 E (E ≥ 0)가 주어진다.

다음 E개 줄에는 귀신 구멍의 정보를 나타내는 X1, Y1, X2, Y2, T 가 주어진다.

(X1, Y1)은 귀신 구멍의 위치이고, (X2, Y2)는 그 귀신 구멍으로 들어갔을 때, 나오는 곳의 위치이다. (0 ≤ X1, X2 < W, 0 ≤ Y1, Y2 < H) (X1,Y1)과 (X2,Y2)가 같을 수도 있다.

T는 귀신 구멍에서 나오는데 걸리는 시간이다. (-10,000 ≤ T ≤ 10,000)

 

두 귀신 구멍이 같은 장소에 있거나, 구멍에서 나오는 지점이 묘비인 경우는 없다.

묘비와 귀신 구멍이 (0, 0)이나 (W-1, H-1)에 있는 경우는 없다.

 

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

 

출력

더보기

각 테스트 케이스 마다 상근이가 과거로 계속해서 돌아간다면 "Never"를 출력.

출구를 빠져나올 수 없으면 "Impossible"을 출력.

그 외의 경우에는 묘지를 빠져나오는데 가장 빠른 시간을 출력.

 

접근법

그리드 형태의 묘지이지만, 쉽게 그래프 형태로 바꿀 수 있음을 알 수 있다.

각 잔디가 하나의 노드가 되고, 인접한 잔디끼리 양방향 간선을 그려준 뒤, 귀신 구멍의 경우에는 구멍 입구 노드에서 출구 노드로 향하는 단방향 간선을 그려주면 된다.

묘비의 경우 갈 수 있는 방법이 없으므로 무시하면 된다.

위의 묘지를 그래프로 바꿈

인접한 잔디 사이의 간선의 cost는 1이고, 귀신 구멍의 간선의 cost는 입력에 따라 다르다.

 

아무튼 그래프 형태로 바꾸기만 하면, 음수 간선이 존재할 수 있고 (귀신 구멍), 음수 간선 사이클이 존재할 수 있는 (상근이가 영원히 과거로 되돌아 갈 수 있는) 경우에서의 최단 경로 찾기 문제임을 쉽게 알 수 있다.

 

벨만-포드 알고리즘을 적용해주기만 하면 끝.

 

벨만-포드 알고리즘을 적용하기 위해서는 edge list가 필요하기 때문에, 주어진 묘지 형태에서 edge만 뽑아내면 되는데, 이 부분에서 생각해야 할 부분이 몇개 있다.

 

  1. 인접한 동서남북의 잔디로는 자유롭게 이용 가능하다. 즉 잔디 사이의 간선은 양방향 간선이다.
  2. 귀신 구멍이 있는 칸으로 가면 반드시 귀신 구멍의 출구로 나온다. 즉 귀신 구멍을 무시하고 지나갈 수 없다. 다시 말하면, 귀신 구멍이 있는 칸에서 인접한 칸에 대한 edge를 list에 추가하면 안된다.
  3. 출구에 도착하면 바로 탈출한다고 했으므로, edge를 뽑아낼 때 출구에서 인접한 칸으로 이동하는 edge는 추가하면 안된다.

 

이 3가지 경우를 고려하며 edge list를 뽑아낸 뒤, 벨만-포드 알고리즘을 그대~로 적용만 해주면 풀린다.

 

edge list를 만드는 함수

static void add_edges()
{
	int[] mr = {-1, 1, 0 ,0};
	int[] mc = {0, 0, -1, 1};

	for (int i = 0; i < H; i ++)
	{
		for (int j = 0; j < W; j++)
		{
			//묘비 or 귀신구멍 or 출구에 대해서는 인접한 칸에 대한 간선 추가하지 않음
			if (map[i][j] != GRASS)
				continue;

			//모든 잔디에서 인접한 칸으로 이동 가능하면 간선 추가
			for (int k = 0; k < 4; k++)
			{
				int tr = i + mr[k];
				int tc = j + mc[k];

				if (can_move_to(tr, tc))
					edges.add(new Edge(new Point(i, j), new Point(tr, tc), 1));
			}
		}
	}
}

 

벨만-포드 알고리즘으로 답을 구하는 함수

static void get_cost_bf()
{
	//벨만 포드 알고리즘에서 cost를 기록할 배열
	cost = new long[H][W];
	for (int i = 0; i < H; i ++)
		for (int j = 0; j < W; j++)
			cost[i][j] = INF; //전부 INF로 초기화

	//출발점의 cost는 0으로 초기화
	cost[0][0] = 0;

	//묘비를 제외한 칸 개수. 즉 그래프의 전체 노드 개수를 의미함.
	int V = W * H - G;

	//V-1번 edge list를 선회
	for (int i = 0; i < V - 1; i++)
	{
		for (Edge e : edges)
		{
			Point start = e.start;
			Point end = e.end;

			//start가 귀신 구멍인데 cost가 음수인 경우
			//start의 cost가 INF이면 아직 start에 도달한적이 한번도 없는데
			//end의 cost가 INF인 경우 end의 cost가 업데이트 되는 경우가 생김
			//그 것을 막기 위해 조건을 걸어둠
			if (cost[start.row][start.col] == INF)
				continue;

			//cost 업데이트
			cost[end.row][end.col] =
				Math.min(cost[end.row][end.col], cost[start.row][start.col] + e.ost);
		}
	}

	//한번 더 cost를 업데이트함
	//바뀌는게 있다는 것은 음수 싸이클이 존재한다는 뜻
	for (Edge e : edges)
	{
		Point start = e.start;
		Point end = e.end;

		if (cost[start.row][start.col] == INF)
			continue;

		if (cost[end.row][end.col] > cost[start.row][start.col] + e.cost)
		{
			result.add("Never");
			return;
		}
	}

	//출구에 도달하지 못한 경우
	if (cost[H - 1][W - 1] == INF)
		result.add("Impossible");
	else
		result.add(Long.toString(cost[H - 1][W - 1]));
}

 

시간 복잡도

edge list를 뽑는 경우에는 묘지를 한번만 순회하면 되므로 W * H

 

벨만-포드 알고리즘의 경우 O(VE)인데

이 문제에서 V = W * H - G, E는 입력에 따라 달라진다.

즉 O((W * H - G)E) 인데 W와 H가 크지 않으므로 제한 시간 내에 풀 수 있다.

 

 

공간 복잡도

W와 H가 크지 않으므로 신경 안써도 된다.

 

 

풀면서 놓쳤던 점

위에서 언급했던

  1. 인접한 동서남북의 잔디로는 자유롭게 이용 가능하다. 즉 잔디 사이의 간선은 양방향 간선이다.
  2. 귀신 구멍이 있는 칸으로 가면 반드시 귀신 구멍의 출구로 나온다. 즉 귀신 구멍을 무시하고 지나갈 수 없다. 다시 말하면, 귀신 구멍이 있는 칸에서 인접한 칸에 대한 edge를 list에 추가하면 안된다.
  3. 출구에 도착하면 바로 탈출한다고 했으므로, edge를 뽑아낼 때 출구에서 인접한 칸으로 이동하는 edge는 추가하면 안된다.

이 3가지를 놓쳤었고,

 

묘지를 탈출하지 못하는 경우와 영원히 과거로 돌아가지 못하는 경우를 애매하게 구분했어서 몇번 더 틀렸다.

 

V - 1번 반복을 했을 때 출구에 도달하지 못했으면 그냥 탈출하지 못하는 경우로 처리했는데, 이 때 음수 싸이클이 존재하는 경우가 있어서 이런 경우는 영원히 과거로 가는 경우 처리를 해야했다.

 

 

이 문제에서 얻어갈 것

벨만-포드 알고리즘의 응용

그래프가 아닌 입력을 그래프 형태로 바꾸는 방법

 

 

코드

import java.util.Scanner;
import java.util.LinkedList;

public class Main
{
	static int W, H, G, E;
	static int[][] map;
	static long[][] cost;
	static LinkedList<Edge> edges;
	static LinkedList<String> result;

	static final int GRASS = 0;
	static final int GRAVE = 1;
	static final int HOLE = 2;
	static final int EXIT = 3;

	static final long INF = Integer.MAX_VALUE;

	public static void main(String[] args)
	{
		Scanner sc = new Scanner(System.in);

		result = new LinkedList<String>();

		while (true)
		{
			W = sc.nextInt();
			H = sc.nextInt();

			if (W == 0 && H == 0)
				break;

			//초깃값 0 은 GRASS를 의미
			map = new int[H][W];

			//출구 표시
			map[H - 1][W - 1] = EXIT;

			G = sc.nextInt();

			//묘비 입력 받음
			int row, col;
			for (int i = 0; i < G; i++)
			{
				col = sc.nextInt();
				row = sc.nextInt();
				map[row][col] = GRAVE;
			}

			E = sc.nextInt();

			//벨만 포드에 쓰일 edge list
			edges = new LinkedList<Edge>();

			//귀신 구멍을 입력받는 동시에 귀신 구멍 간선 추가
			int row1, col1, row2, col2, t;
			for (int i = 0; i < E; i++)
			{
				col1 = sc.nextInt();
				row1 = sc.nextInt();
				col2 = sc.nextInt();
				row2 = sc.nextInt();
				t = sc.nextInt();

				map[row1][col1] = HOLE;

				edges.add(new Edge(new Point(row1, col1), new Point(row2, col2), t));
			}

			//edge list에 edge를 마저 추가
			add_edges();

			//벨만포드 적용
			get_cost_bf();
		}

		for (String s : result)
		    System.out.println(s);
	}

	static void get_cost_bf()
	{
		//벨만 포드 알고리즘에서 cost를 기록할 배열
		cost = new long[H][W];
		for (int i = 0; i < H; i ++)
			for (int j = 0; j < W; j++)
				cost[i][j] = INF; //전부 INF로 초기화

		//출발점의 cost는 0으로 초기화
		cost[0][0] = 0;

		//묘비를 제외한 칸 개수. 즉 그래프의 전체 노드 개수를 의미함.
		int V = W * H - G;

		//V-1번 edge list를 선회
		for (int i = 0; i < V - 1; i++)
		{
			for (Edge e : edges)
			{
				Point start = e.start;
				Point end = e.end;

				//start가 귀신 구멍인데 cost가 음수인 경우
				//start의 cost가 INF이면 아직 start에 도달한적이 한번도 없는데
				//end의 cost가 INF인 경우 end의 cost가 업데이트 되는 경우가 생김
				//그 것을 막기 위해 조건을 걸어둠
				if (cost[start.row][start.col] == INF)
					continue;

				//cost 업데이트
				cost[end.row][end.col] =
					Math.min(cost[end.row][end.col], cost[start.row][start.col] + e.cost);
			}
		}

		//한번 더 cost를 업데이트함
		//바뀌는게 있다는 것은 음수 싸이클이 존재한다는 뜻
		for (Edge e : edges)
		{
			Point start = e.start;
			Point end = e.end;

			if (cost[start.row][start.col] == INF)
				continue;

			if (cost[end.row][end.col] > cost[start.row][start.col] + e.cost)
			{
				result.add("Never");
				return;
			}
		}

		//출구에 도달하지 못한 경우
		if (cost[H - 1][W - 1] == INF)
			result.add("Impossible");
		else
			result.add(Long.toString(cost[H - 1][W - 1]));
	}

	static void add_edges()
	{
		int[] mr = {-1, 1, 0 ,0};
		int[] mc = {0, 0, -1, 1};

		for (int i = 0; i < H; i ++)
		{
			for (int j = 0; j < W; j++)
			{
				//묘비 or 귀신구멍 or 출구에 대해서는 인접한 칸에 대한 간선 추가하지 않음
				if (map[i][j] != GRASS)
					continue;

				//모든 잔디에서 인접한 칸으로 이동 가능하면 간선 추가
				for (int k = 0; k < 4; k++)
				{
					int tr = i + mr[k];
					int tc = j + mc[k];

					if (can_move_to(tr, tc))
						edges.add(new Edge(new Point(i, j), new Point(tr, tc), 1));
				}
			}
		}
	}

	static boolean can_move_to(int row, int col)
	{
		return (row >= 0 && row < H && col >= 0 && col < W && map[row][col] != GRAVE);
	}

}

class Point
{
	int row;
	int col;

	public Point(int row, int col)
	{
		this.row = row;
		this.col = col;
	}
}

class Edge
{
	Point start;
	Point end;
	int cost;

	public Edge(Point start, Point end, int cost)
	{
		this.start = start;
		this.end = end;
		this.cost = cost;
	}
}

댓글