문제 풀이/알고리즘

[백준] (1194번) 달이 차오른다, 가자

얌얌념념 2022. 4. 1. 15:15

문제 (1194번) 달이 차오른다, 가자

링크 : https://www.acmicpc.net/problem/1194

사용 언어 : JAVA



내 생각

초기 생각
  • 옛날에 풀었던 아기상어와 비슷하게 풀면 되겠다고 생각했다.
  • 근데 visited 배열을 어떻게 만들어야할지 당최.. 알 수가 없었다. (다른 문제를 풀어도 항상 여기서 막힌다..ㅎ)
    그래서 위의 방법이 아닌가? 하면서 dfs로도 풀어보고 stack도 사용해보고 여튼 별 짓 다함
개선
  • 비트 연산자를 활용해야 한다는 힌트를 듣고, visited 배열에 key 보유 상태를 나타내는 차원을 추가했다.
    3차원 배열을 사용할 때마다 안터지나..? 하고 조마조마한데 생각보다 잘 안터지는 듯.
최종

bfs/dfs 문제는 푸는 방식이 거의 정해져 있다.

  1. bfs로 탐색. bfs 큐에 상태를 하나 꺼내서, 여기에서 나올 수 있는 다음 경우의 수를 탐색.
    근데 만약 꺼낸 상태노드가 이전에 방문했었던 노드라면 탐색하지 않고 다음으로 스킵.
  2. 방문 안한 노드라면 방문 표시를 해주고 현재 필드 값으로 추가적인 액션 취함.
  3. bfs 큐에 다음 상태를 삽입.

여기에 추가적으로 사용된 아이디어는 다음과 같다.

  • 몇번의 depth 때 1에 도달하는지 계산하기 위해 bfs용 queue를 2개 사용

depth = i의 노드를 담은 Qcur에서 꺼낸 노드 Ncur가 있을 때, 그 Ncur에서 나아갈 수 있는 다음 노드들을 Qnext에 집어넣는다. 모든 Qcur의 노드에서 이런 과정을 거쳤다면, 결과적으로 Qnext에 담긴 모든 노드는 Qcur에 속한 노드들의 자식들을 모아놓은 큐가 된다. 따라서 두 큐 사이의 depth는 1의 차이가 난다. 이것을 활용해 최단 경로의 길이를 구할 수 있다.

  • visited[row][col][key status]

key status가 왜 필요하냐? 바로 문제에 나온 '열쇠''문'이라는 제약조건 때문이다.

bfs 탐색을 중단해야하는 경우는 왔던 방향을 제외한 삼방이 , (열 수 없는) 으로 막히거나 맵 가장자리어서 움직일 수 없는 경우이다. 보통은 여기까지 오면 다른 쪽으로 갈 수 있는 방법이 없으니 쿨하게 중단하고 다음 탐색을 시작하면 되지만, 이 문제는 '문을 열면 진행되는 경우'가 있기 때문에 만약 내가 열쇠를 가지고 있다면 왔던 길을 되돌아가서 그 문을 열어봐야 한다는 것이다.

진행할 수 없지만 탐색을 끝내면 안된다.
왔던 길을 되돌아가서 내가 열 수 있는 문이 있는지 찾아야 한다.
벽이나 문으로 막힌 경우. 열쇠가 없으니까 쿨하게 보내준다.

그러니까, 원래같으면 방문했던 길은 다음 탐색 후보에서 쳐내야 했는데 이제는 한번 방문했던 길을 다시 방문할 수도 있기 때문에 위치 정보만으로 탐색 후보에서 쳐낼 수 없는 상황이다. 때문에 '이 노드를 다시 탐색할 것인가?' 에 대한 새로운 기준이 하나 더 필요해졌다. 그리고 그 해결책이 key 보유 정보를 담는 것이다.

위의 경우에는 visited[2][1][0], visited[1][1][0]을 true로 만들면서 a로 이동한다. 그리고 a에 도달했을 때 새로운 key를 얻으면서 key 보유 상태값이 바뀐다. 그러면 이제부터 visited[위치][0]는 버려지고 새로운 visited[위치]['a']를 사용하게 된다! 그러면 map[0][0]에서 내려오는 것이 가능해진다. (visited[2][1]['a'], visited[1][1]['a']는 false이니까)

위처럼 key 보유 상태값에 변화가 없다면? 계속 visited[위치][0]를 참조할 것이므로 더이상 움직일 수 없어 탐색이 중단될 것이다.

  • key 보유 상태를 나타내기 위한 비트연산자 사용

visited의 3번째 항목 크기를 {1<<(key 종류 개수)}만큼 만들어준다. 여기에 000000(2) 값으로 초기화해주고, 새로운 key를 주웠을 때 1<<(key-'a') 값을 더해준다. 

 

최종 코드

더보기
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.LinkedList;
import java.util.Queue;
import java.util.StringTokenizer;

public class Main_BJ_1194_달이차오른다가자 {

	// r,c : 노드의 위치, key: 노드의 key 보유 상태
	static class Node {
		int r, c;
		int key;
		public Node(int r, int c, int key) {
			this.r = r;
			this.c = c;
			this.key = key;
		}
	}
	
	public static void main(String[] args) throws NumberFormatException, IOException {
		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());	// 미로의 너비
			
		char maze[][] = new char[N][M];
		
		int start[] = new int[2];	// 시작 위치 (0)
		for(int i=0; i<N; i++) {
			String line = br.readLine();
			maze[i] = br.readLine().toCharArray();
			if(line.contains("0")) {
				start[0] = i;
				start[1] = line.indexOf('0');
			}
		}
		System.out.println(findWayExit(maze, start));
	}
	
    // 차례로 상, 우, 하, 좌
	static int dr[] = {-1, 0, 1, 0};
	static int dc[] = {0, 1, 0 ,-1};

	private static int findWayExit(char[][] maze, int[] start) {
		int r = maze.length;
		int c = maze[0].length;
		
        /*	visited
		 * [r][c]: 방문 위치
		 * [1<<6]: 키가 A, B, C, D, E, F 총 6가지 이므로 이 키들의 보유 여부를 나타낼 수 있는 비트가 필요하다.
		 * 			-> 6비트짜리 int를 담을 수 있는 크기로 만듦
		 */
		boolean[][][] visited = new boolean[r][c][1<<6];
		
		// 현재 이동 가능한 노드를 담는 큐
		Queue<Node> current = new LinkedList<>();
		// 맨 처음 시작 위치(start[])와 처음 key 보유 상태(000000)를 담은 노드 객체를 넣어준다.
		current.add(new Node(start[0], start[1], 0));
		
        // bfs의 깊이를 나타냄
		int dist = 0;
		
		while(!current.isEmpty()) {
			++dist;
			// 현재 노드에서 이동할 수 있는 다음 상태를 담는 큐
			Queue<Node> next = new LinkedList<>();
			while(!current.isEmpty()) {
				
				// 현재 큐에서 뽑아낸 상태
				Node cur = current.poll();
				// 현재 상태에서 사방으로 탐색
				for(int m=0; m<4; m++) {
					int nr = cur.r+dr[m];
					int nc = cur.c+dc[m];
					int nkey = cur.key;
					
					// 다음 노드가 미로를 벗어나면 탐색 중단
					if(0>nr || nr>=r || 0>nc || nc>=c)	continue;
				
					// 다음 노드의 필드 값
					char pos = maze[nr][nc];
					
					if(pos == '1')		return dist;		// 종료지점일 경우 bfs의 깊이를 리턴
					if(pos == '#')		continue;		// 진행할 수 없는 벽일 경우 탐색 중단
					
					if(visited[nr][nc][nkey])	continue;	// 현재 상태에서 방문했던 노드일 경우 탐색 중단
					visited[nr][nc][nkey] = true;			// 방문 안했었다면 탐색. 먼저 방문 표시 해줌

					// 현재 위치에 키가 있다면 무조건 줍는다.
					if('a'<=pos && pos<='z') {
						// 현재 노드가 가진 키 상태에 지금 얻은 키 정보를 넣고 다음 탐색 큐에 추가
						nkey |= 1<<(pos-'a');
						next.add(new Node(nr, nc, nkey));
					}
					// 현재 위치에 문이 있다면
					else if('A'<=pos && pos<='Z') {
						// 현재 노드가 그 문의 열쇠를 가진 경우에만 다음 탐색 큐에 추가
						if((nkey & 1<<(pos-'a')) != 0){
							next.add(new Node(nr, nc, nkey));
						}
					}
					// 그 외 빈칸이나 시작점일 경우 다음 탐색 큐에 추가
					else if(pos == '.' || pos == '0') {
						next.add(new Node(nr, nc, nkey));
					}
				}
			}
			// 현재 이동할 수 있는 노드를 다 탐색했다면 다음 탐색할 노드를 current 큐에 담음
			current = next;
		}
		// 탐색할 수 있는 모든 노드를 다 돌았음에도 중간에 1을 만나지 못했다면 -1 반환
		return -1;
	}
}

느낀점

  • visited를 어떻게 설정해야 하는지가 정말 어려운 것 같다.. 더...... 열심히 푸는 걸로.............