본문 바로가기

코딩테스트 연습/백준

[ 백준 / 19238번 ] Gold IV - 스타트 택시 ( java )

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

문제

스타트링크가 "스타트 택시"라는 이름의 택시 사업을 시작했다. 스타트 택시는 특이하게도 손님을 도착지로 데려다줄 때마다 연료가 충전되고, 연료가 바닥나면 그 날의 업무가 끝난다.

택시 기사 최백준은 오늘 M명의 승객을 태우는 것이 목표이다. 백준이 활동할 영역은 N×N 크기의 격자로 나타낼 수 있고, 각 칸은 비어 있거나 벽이 놓여 있다. 택시가 빈칸에 있을 때, 상하좌우로 인접한 빈칸 중 하나로 이동할 수 있다. 알고리즘 경력이 많은 백준은 특정 위치로 이동할 때 항상 최단경로로만 이동한다.

M명의 승객은 빈칸 중 하나에 서 있으며, 다른 빈칸 중 하나로 이동하려고 한다. 여러 승객이 같이 탑승하는 경우는 없다. 따라서 백준은 한 승객을 태워 목적지로 이동시키는 일을 M번 반복해야 한다. 각 승객은 스스로 움직이지 않으며, 출발지에서만 택시에 탈 수 있고, 목적지에서만 택시에서 내릴 수 있다.

백준이 태울 승객을 고를 때는 현재 위치에서 최단거리가 가장 짧은 승객을 고른다. 그런 승객이 여러 명이면 그중 행 번호가 가장 작은 승객을, 그런 승객도 여러 명이면 그중 열 번호가 가장 작은 승객을 고른다. 택시와 승객이 같은 위치에 서 있으면 그 승객까지의 최단거리는 0이다. 연료는 한 칸 이동할 때마다 1만큼 소모된다. 한 승객을 목적지로 성공적으로 이동시키면, 그 승객을 태워 이동하면서 소모한 연료 양의 두 배가 충전된다. 이동하는 도중에 연료가 바닥나면 이동에 실패하고, 그 날의 업무가 끝난다. 승객을 목적지로 이동시킨 동시에 연료가 바닥나는 경우는 실패한 것으로 간주하지 않는다.

<그림 1>

<그림 1>은 택시가 활동할 영역의 지도를 나타내며, 택시와 세 명의 승객의 출발지와 목적지가 표시되어 있다. 택시의 현재 연료 양은 15이다. 현재 택시에서 각 손님까지의 최단거리는 각각 9, 6, 7이므로, 택시는 2번 승객의 출발지로 이동한다.

<그림 2>

<그림 3>

<그림 2>는 택시가 2번 승객의 출발지로 가는 경로를, <그림 3>은 2번 승객의 출발지에서 목적지로 가는 경로를 나타낸다. 목적지로 이동할 때까지 소비한 연료는 6이고, 이동하고 나서 12가 충전되므로 남은 연료의 양은 15이다. 이제 택시에서 각 손님까지의 최단거리는 둘 다 7이므로, 택시는 둘 중 행 번호가 더 작은 1번 승객의 출발지로 이동한다.

<그림 4>

<그림 5>

<그림 4>와 <그림 5>는 택시가 1번 승객을 태워 목적지로 이동시키는 경로를 나타낸다. 남은 연료의 양은 15 - 7 - 7 + 7×2 = 15이다.

<그림 6>

<그림 7>

<그림 6>과 <그림 7>은 택시가 3번 승객을 태워 목적지로 이동시키는 경로를 나타낸다. 최종적으로 남은 연료의 양은 15 - 5 - 4 + 4×2 = 14이다.

모든 승객을 성공적으로 데려다줄 수 있는지 알아내고, 데려다줄 수 있을 경우 최종적으로 남는 연료의 양을 출력하는 프로그램을 작성하시오.

입력

첫 줄에 N, M, 그리고 초기 연료의 양이 주어진다. (2 ≤ N ≤ 20, 1 ≤ M ≤ N2, 1 ≤ 초기 연료 ≤ 500,000) 연료는 무한히 많이 담을 수 있기 때문에, 초기 연료의 양을 넘어서 충전될 수도 있다.

다음 줄부터 N개의 줄에 걸쳐 백준이 활동할 영역의 지도가 주어진다. 0은 빈칸, 1은 벽을 나타낸다.

다음 줄에는 백준이 운전을 시작하는 칸의 행 번호와 열 번호가 주어진다. 행과 열 번호는 1 이상 N 이하의 자연수이고, 운전을 시작하는 칸은 빈칸이다.

그다음 줄부터 M개의 줄에 걸쳐 각 승객의 출발지의 행과 열 번호, 그리고 목적지의 행과 열 번호가 주어진다. 모든 출발지와 목적지는 빈칸이고, 모든 출발지는 서로 다르며, 각 손님의 출발지와 목적지는 다르다.

출력

모든 손님을 이동시키고 연료를 충전했을 때 남은 연료의 양을 출력한다. 단, 이동 도중에 연료가 바닥나서 다음 출발지나 목적지로 이동할 수 없으면 -1을 출력한다. 모든 손님을 이동시킬 수 없는 경우에도 -1을 출력한다.

 

풀이 코드를 넣고, 이후 설명을 하겠다.

import java.util.*;
import java.io.*;

public class 스타트택시 {
	public static void main(String[] args) throws Exception {
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
		StringTokenizer st =new StringTokenizer(br.readLine());
		int N = Integer.parseInt(st.nextToken());
		int M = Integer.parseInt(st.nextToken());
		int Y = Integer.parseInt(st.nextToken());
		int[][] arr  = new int[N][N];
		for(int i = 0; i<N;i++) {
			st = new StringTokenizer(br.readLine());
			for(int j = 0; j<N;j++) {
				arr[i][j] = Integer.parseInt(st.nextToken());
			}
		}
		st =new StringTokenizer(br.readLine());
		int R = Integer.parseInt(st.nextToken())-1;
		int C = Integer.parseInt(st.nextToken())-1;
		int[] dr = new int[M+2];
		int[] dc = new int[M+2];
		int [] dy = new int[M+2];
		for(int i = 0; i<M+2;i++) {
			dy[i] = Integer.MAX_VALUE;
		}
		for(int i = 0; i<M;i++) {
			st = new StringTokenizer(br.readLine());
			int r = Integer.parseInt(st.nextToken())-1;
			int c = Integer.parseInt(st.nextToken())-1;
			dr[i+2]= Integer.parseInt(st.nextToken())-1;
			dc[i+2] = Integer.parseInt(st.nextToken())-1;
			arr[r][c] = i+2;
			Queue<int[]> q = new LinkedList<int[]>();
			q.add(new int[] {r,c,0});
			boolean[][] v = new boolean[N][N];
			v[r][c] =true;
			while(!q.isEmpty()) {
				int[] t = q.poll();
				//System.out.println(Arrays.toString(t));
				r = t[0];c = t[1];int y = t[2];
				if(dr[i+2]==r && dc[i+2]==c) {
					dy[i+2]= y;
					break;
				}
				y++;
				if(r>0 && arr[r-1][c]!=1 && !v[r-1][c]) {q.add(new int[]{r-1,c,y});v[r-1][c]=true;}
				if(c>0 && arr[r][c-1]!=1 && !v[r][c-1]) {q.add(new int[] {r,c-1,y});v[r][c-1]=true;}
				if(c<N-1 && arr[r][c+1]!=1 && !v[r][c+1]) {q.add(new int[] {r,c+1,y});v[r][c+1]=true;}
				if(r<N-1 && arr[r+1][c]!=1 && !v[r+1][c]) {q.add(new int[] {r+1,c,y});v[r+1][c]=true;}
			}
		}
		PriorityQueue<int[]> pq = new PriorityQueue<int[]>(new Comparator<int[]>() {

			@Override
			public int compare(int[] o1, int[] o2) {
				if(o1[2]==o2[2]) {
					if(o1[0]==o2[0])return o1[1]-o2[1];
					return o1[0]-o2[0];
				}
				return o2[2]-o1[2];
			}
		}) ;
		pq.add(new int[] {R,C,Y});
		boolean [][] v = new boolean[N][N];
		v[R][C]= true;
		while(!pq.isEmpty()) {
			int r = pq.peek()[0];
			int c = pq.peek()[1];
			int y = pq.poll()[2];
			//System.out.println(r + " " + c + " "+y);
			if(arr[r][c]>1) {
				int i = arr[r][c];
				if(y<dy[i]) {
					System.out.print(-1);
					System.exit(0);
				}
				pq.clear();
				v  = new boolean[N][N];
				v[dr[i]][dc[i]] = true;
				pq.add(new int[] {dr[i],dc[i],y+dy[i]});
				arr[r][c] = 0;
				if(--M==0) {
					Y =y+dy[i];
					break;
				}
			}
			else {
				y--;
				if(r>0 && arr[r-1][c]!=1 && !v[r-1][c]) {pq.add(new int[]{r-1,c,y});v[r-1][c]=true;}
				if(c>0 && arr[r][c-1]!=1 && !v[r][c-1]) {pq.add(new int[] {r,c-1,y});v[r][c-1]=true;}
				if(c<N-1 && arr[r][c+1]!=1 && !v[r][c+1]) {pq.add(new int[] {r,c+1,y});v[r][c+1]=true;}
				if(r<N-1 && arr[r+1][c]!=1 && !v[r+1][c]) {pq.add(new int[] {r+1,c,y});v[r+1][c]=true;}
			}
			
		}
		if(M!=0)System.out.println(-1);
		else System.out.println(Y);
		//83%에서 틀렸다고 뜬다..-> 해결!
		
		
		
		
	}
}

 총 줄바꿈을 전부 한게 아니더라도 길고 긴 코드이다...

여기서 사용하는 알고리즘을 하나씩 뽑아서 구현하여야하는데, 먼저 이 문제 설명을 읽고 파악한 것은 

최단 거리를 찾아서 택시가 이동한다는 점이다. 이 부분이 이 알고리즘이 해결하는 핵심이다.

여기에서 최단 거리를 찾는 방법은 BFS(넓이우선탐색)을 진행한다. 

여기에서 두가지 방법으로 탐색이 진행되는데, 

1. 택시가 최단거리 승객 탐색(같은 거리라면 행렬순 ) 

2. 승객이 목적지로 최단거리 이동( 이부분이 83%에서 틀리게한 주범이었다.)

이 발생한다.

이를 구현하고자, 우선 2를 먼저 구현하였다. 

미리 최단 거리를 계산해 두고, 사실 이동과정과 목적지는 이후로 필요 없으니, 목적지까지 소요되는 비용이 중요하게 이용된다. 

int[] dr 은 목적지의 행 , dc는 목적지의 열, 그리고 dy는 목적지까지 소요되는 연료이다.

이를 해결하기 위해서 

목적지까지 최단 거리로 가는 방법 

bfs로 최단거리를 탐색해 나가면서, 가장 연료를 적게 쓰면서 목적지를 찾는 방법을 탐색한다. 

위 bfs는 비슷하게 이후 택시가 최단거리를 찾는 경우에도 복사해서 사용하여 편리하였다.

여기서 중요한 점이

승객이 목적지 까지 갈 수 없는 방법도 있다는 점이다. 그런 경우 최대값을 목적지에 넣어 주었다. (진짜 이거 찾아낸 나 너무 칭찬해ㅠㅠㅠ 몰랐으면 포기할 뻔 했다ㅠㅠㅠ)

여기서 추가 설명으로 데이터를 받을 떄 M+2의 배열로 선언한 이유는 arr지도로 확인을 하면서 가기 위하여 승객을 2~M+2까지의 승객을 숫자로 넣어 주었기 때문이다.

이후 이러한 값을 먼저 가지고 가면 승객 (arr[r][c]>=2)을 찾은 경우 바로 연료에서 dy[arr[r][c]]를 더하는 방식으로 해결할 수 있다. 

이후 택시의 시작점부터 최단 거리에 있는 승객을 PriorityQueue로 탐색하면서 진행한다.

PriorityQueue를 사용하여야 행과 열에 따라 승객을 선택할 수 있게 된다. 

PriorityQueue를 비교 기준과 함께 선언 

Queue에는 현재 r의 값(행 위치) , (열 위치) c, y(현재 남은 연료) 가 저장된다. 

연료가 많다는 것은 가까이 있다는 것을 의미하므로 y(역순: o1[2]), r(o1[0]), c(o1[1])식으로 정렬이 된다. 

택시가 가장 가까운 승객을 찾는 BFS

이후 승객의 목적지를 찾는 방식과 동일하게 Queue로 BFS를 돌면서 탐색하게 된다. 

이떄 앞선 승객이 목적지 까지 가는 방식을 가지고 있기 때문에, arr[r][c]>=2인 승객의 위치를 확인 하였을 경우 

택시가 승객을 만났을 경우 이용

만약 목적지까지 갈 연료가 없다면, -1을 출력하고, 프로그램을 끝낸다. 

아니라면 ,이전에 담겨져 있던 값과, 방문 확인을 초기화 하고, pq에 승객을 태우고 도착할 목적지, 그리고 목적지에서 충전되는 연료를 합쳐 pq에 넣어주게된다. 

이 과정에서 모든 승객을 태웠는지 확인하기 위하여 앞서 받아온 M에서 1씩을 뺴주면서 검사를 하게 되는데, 승객이 0일 경우, 처음에 남은 연료로 사용했던 Y변수에 y+dy[i]인 연료 양을 넣고, 루프를 빠져나오게 된다. ( 사실 바로 출력하고 프로그램을 끝내도 된다. )

만약 이 과정 없이 loop가 끝났다면,택시가 승객을 태울 수 없는 경우이기 때문에, M의 값을 다시 검사해 결과를 출력한다. 

최종 값 출력 

이중 BFS를 두번 사용하는 것이 이 문제 해결의 핵심이라고 할 수 있겠다.