[BOJ] - 달이 차오른다, 가자. / 1194번 (Java)
[BOJ] - 달이 차오른다, 가자. / 1194번 (Java)
1. 문제 접근
문제의 궁극적인 목표는 '0'(출발지)에서 '가장 가까운 1'(도착지)로 갈 때, 이동 횟수의 최솟값을 구하는 것이다.
단, 몇가지 제약 사항이 존재한다.
- 벽으로는 절대 이동할 수 없다.
- 빈칸은 언제나 이동할 수 있다.
- 총 6개의 열쇠('a', 'b', 'c', 'd', 'e', 'f')가 존재하고, 각 열쇠는 6개의 문( 'A', 'B', 'C', 'D', 'E', 'F' )를 열 수 있다.
즉, 열쇠를 얻고, 문을 여는 것에 대한 탐색을 어떻게 할 것인지가 관건이다.
탐색을 진행하며 만나는 중요한 지점은 2가지다.
- 4방 탐색을 진행한 결과 문을 만난다.
- 4방 탐색을 진행한 결과 열쇠를 만난다.
문을 만나게 되면, 확인해야하는 것은 "현재 상태에 해당 문에 대한 열쇠를 가지고 있는가?" 이다.
하지만 현재 열쇠가 없다고 미래에 역시 없다는 것은 보장할 수 없다.
이후 BFS 탐색을 진행하며 해당 문에 맞는 열쇠를 찾을 수도 있기 떄문이다.
따라서 열쇠를 만나게 되면 지금까지 탐색을 마친 지점이라도 열 수 있는 문이 있을 수 있기에 다시 탐색을 진행해야한다.
맵의 크기는 50 X 50으로 최대 2,500의 크기이며, 6개의 열쇠로 구성할 수 있는 열쇠의 조합은 총 64개가 된다.
따라서 높이가 64로 고정인 3차원 방문 배열(`isVisited[64][N][M]`)를 사용하면 된다.
해당 배열이 의미하는 것은 "[N, M]위치에 (0 ~ 63)번째 열쇠 조합을 소지하고 방문한 적이 있는가?"이다.
열쇠 조합의 경우 비트마스킹을 사용하면 쉽게 접근 할 수 있다.
열쇠마다 1bit씩 총 6bit 할당하게 되며, a = 1 / b = 2 / c = 4 / d = 8 / e = 16 / f = 32 와 같은 형태가 된다.
예를 들어 비트 상태가 "0 1 1 1 0 0"이라면, 이는 c, d, e 열쇠를 소지한 상태라는 것을 의미한다.
따라서 열쇠를 만난다면 만난 열쇠와 비트OR 연산을 통해 합산한 뒤 다시 탐색을 진행하면 된다.
static final char[] keys = {'a', 'b', 'c', 'd', 'e', 'f'};
char nextChar = map[nextRow][nextCol];
// 4. 키
for (int idx = 0; idx < keys.length; ++idx) {
if(nextChar == keys[idx]) {
// 키 합치기
nextKeyStatus = (int) Math.pow(2, (int) doors[idx] - 'A') | nextKeyStatus;
if(!isVisited[nextKeyStatus][nextRow][nextCol]) {
isVisited[nextKeyStatus][nextRow][nextCol] = true;
q.offer(new State(nextRow, nextCol, nextDist, nextKeyStatus));
}else {
break;
}
}
}
또한, 문을 만난다면 현재 가지고 있는 열쇠 조합과 비트AND 연산을 통해 0보다 크다면 문을 열 수 있다고 판단할 수 있다.
static final char[] doors = {'A', 'B', 'C', 'D', 'E', 'F'};
char nextChar = map[nextRow][nextCol];
// 3. 문
for (int idx = 0; idx < doors.length; ++idx) {
if (nextChar == doors[idx]) {
// 현재 그 키를 가지고 있다
if (((int) Math.pow(2, (int) doors[idx] - 'A') & currKeyStatus) > 0) {
if (!isVisited[nextKeyStatus][nextRow][nextCol]) {
isVisited[nextKeyStatus][nextRow][nextCol] = true;
q.offer(new State(nextRow, nextCol, nextDist, nextKeyStatus));
}
}
// 현재 그 키를 가지고 있지 않다
else {
break;
}
}
}
최종적으로 종료지점을 만나면 이동거리를 출력하고 종료한다.
2. 코드
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.ArrayDeque;
import java.util.Queue;
import java.util.StringTokenizer;
/**
* [BOJ] - 달이 차오른다, 가자. / 1194번
* @author JinHxxxxKim
*
* BFS
*
* 조건1: 열쇠를 얻어야 대응하는 문을 들어갈 수 있다.
* 조건2: 0->1로 가는 최단 경로를 구해야한다.
*
* a = 1 / b = 2 / c = 4 / d = 8 / e = 16 / f = 32
*
* 1. 맵 정보를 입력받는다.
* 1-1. 0을 입력 받으면, State Class에 row, col을 저장한다.
* 2. visted 배열은 64 x N x N 의 3차원 배열로 설정한다.
* 3. 열쇠는 비트마스킹을 통해 저장한다.
* 4. 시작점에서 BFS를 실행한다.
* 4-1. 현재 열쇠 조합에서 4방 탐색을 실행한다.
* 4-1-1. 현재 열쇠 조합으로 방문한 적이 없다면 Queue에 offer
* 4-1-2. 현재 열쇠 조합으로 방문한 적이 있다면, PASS
* 4-2. 다음 노드의 경우의 수
* - 벽: PASS
* - 열쇠: 현재 열쇠 조합 | 만나 열쇠 => 새로운 열쇠 조합을 만든다.
* - 문: 현재 열쇠 조합에서 통과할 수 있는 문인지 검증한다.
* - 빈칸: 방문한적이 없다면 Queue offer
* - 출구: 최단 경로를 출력하고 저장한다.
*
*/
public class Solution {
static final BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
static StringTokenizer st;
static final int[] dx = {-1, 1, 0, 0};
static final int[] dy = {0, 0, -1, 1};
static final char[] keys = {'a', 'b', 'c', 'd', 'e', 'f'};
static final char[] doors = {'A', 'B', 'C', 'D', 'E', 'F'};
static final char WALL = '#';
static final char FLOOR = '.';
static final char EXIT = '1';
static char[][] map;
static boolean[][][] isVisited;
static int N, M;
public static void main(String[] args) throws Exception {
st = new StringTokenizer(br.readLine().trim());
N = Integer.parseInt(st.nextToken());
M = Integer.parseInt(st.nextToken());
map = new char[N][M];
isVisited = new boolean[64][N][M]; // 열쇠를 가지고 있을 수 있는 조합은 총 64가지
State startState = null;
// 맵 입력
for (int row = 0; row < N; ++row) {
String rowInfo = br.readLine().trim();
for (int col = 0; col < M; ++col) {
char currChar = rowInfo.charAt(col);
if (currChar == '0') {
// 시작점 저장
startState = new State(row, col, 0, 0);
map[row][col] = '.';
}else {
map[row][col] = currChar;
}
}
}
// 로직 시작
Queue<State> q = new ArrayDeque<>();
q.offer(startState);
isVisited[0][startState.row][startState.col] = true;
while(!q.isEmpty()) {
State currNode = q.poll();
int currRow = currNode.row;
int currCol = currNode.col;
int currDist = currNode.dist;
int currKeyStatus = currNode.keyStatus;
// 4방 탐색
for (int dir = 0; dir < 4; ++dir) {
int nextRow = currRow + dx[dir];
int nextCol = currCol + dy[dir];
int nextDist = currDist + 1;
int nextKeyStatus = currKeyStatus;
// 범위 검증
if (nextCol < 0 || nextRow < 0 || nextCol >= M || nextRow >= N)
continue;
// 나올 수 있는 경우의 수 확인
char nextChar = map[nextRow][nextCol];
// 1. 벽
if (nextChar == WALL) {
continue;
}
// 2. 빈칸
else if (nextChar == FLOOR) {
if (!isVisited[nextKeyStatus][nextRow][nextCol]) {
isVisited[nextKeyStatus][nextRow][nextCol] = true;
q.offer(new State(nextRow, nextCol, nextDist, nextKeyStatus));
}
continue;
}
// 3. 문
for (int idx = 0; idx < doors.length; ++idx) {
if (nextChar == doors[idx]) {
// 현재 그 키를 가지고 있다
if (((int) Math.pow(2, (int) doors[idx] - 'A') & currKeyStatus) > 0) {
if (!isVisited[nextKeyStatus][nextRow][nextCol]) {
isVisited[nextKeyStatus][nextRow][nextCol] = true;
q.offer(new State(nextRow, nextCol, nextDist, nextKeyStatus));
}
}
// 현재 그 키를 가지고 있지 않다
else {
break;
}
}
}
// 4. 키
for (int idx = 0; idx < keys.length; ++idx) {
if(nextChar == keys[idx]) {
// 키 합치기
nextKeyStatus = (int) Math.pow(2, (int) doors[idx] - 'A') | nextKeyStatus;
if(!isVisited[nextKeyStatus][nextRow][nextCol]) {
isVisited[nextKeyStatus][nextRow][nextCol] = true;
q.offer(new State(nextRow, nextCol, nextDist, nextKeyStatus));
}else {
break;
}
}
}
// 5. 출구
if(nextChar == EXIT) {
System.out.println(nextDist);
return;
}
}
}
System.out.println(-1);
}
static class State {
int row, col;
int dist;
int keyStatus;
public State(int row, int col, int dist, int keyStatus) {
super();
this.row = row;
this.col = col;
this.dist = dist;
this.keyStatus = keyStatus;
}
@Override
public String toString() {
return "State [row=" + row + ", col=" + col + ", dist=" + dist + ", keyStatus=" + keyStatus + "]";
}
}
}
3. 디버깅
3차원 방문 배열을 사용하는 것, 비트 마스킹을 통해 조금 더 쉽게 문제에 접근 할 수 있다.
백준 벽 부수고 이동하기 시리즈를 풀어 본 것이 3차원 방문 배열을 사용하는 것에 도움을 많이 준 것 같다.
4. 결과
'알고리즘' 카테고리의 다른 글
[Algorithm] - 위상정렬(Topological Sort) (0) | 2025.04.14 |
---|---|
[BOJ] - 녹색 옷 입은 애가 젤다지? / 4485번 (Java) (1) | 2024.03.27 |
[BOJ] - 평범한 배낭 / 12865번 (Java) (0) | 2024.03.27 |
[BOJ] - RGB거리 / 1149번 (Java) (0) | 2024.02.28 |
[BOJ] - 1로 만들기 / 1463번 (Java) (0) | 2024.02.28 |