문제
N×M의 행렬로 표현되는 맵이 있다. 맵에서 0은 이동할 수 있는 곳을 나타내고, 1은 이동할 수 없는 벽이 있는 곳을 나타낸다. 당신은 (1, 1)에서 (N, M)의 위치까지 이동하려 하는데, 이때 최단 경로로 이동하려 한다. 최단경로는 맵에서 가장 적은 개수의 칸을 지나는 경로를 말하는데, 이때 시작하는 칸과 끝나는 칸도 포함해서 센다.
만약에 이동하는 도중에 한 개의 벽을 부수고 이동하는 것이 좀 더 경로가 짧아진다면, 벽을 한 개 까지 부수고 이동하여도 된다.
한 칸에서 이동할 수 있는 칸은 상하좌우로 인접한 칸이다.
맵이 주어졌을 때, 최단 경로를 구해 내는 프로그램을 작성하시오.
입력
첫째 줄에 N(1 ≤ N ≤ 1,000), M(1 ≤ M ≤ 1,000)이 주어진다. 다음 N개의 줄에 M개의 숫자로 맵이 주어진다. (1, 1)과 (N, M)은 항상 0이라고 가정하자.
출력
첫째 줄에 최단 거리를 출력한다. 불가능할 때는 -1을 출력한다.
풀이
package baekjoon;
import java.io.*;
import java.util.*;
public class BOJ2206 {
static final int[] dx = {-1, 1, 0, 0};
static final int[] dy = {0, 0, -1, 1};
static final int NONE_CHECK = 0;
static final int NONE_COMPLETE_CHECK = 1;
static final int COMPLETE_CHECK = 2;
static BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
static StringTokenizer st;
static int n, m, min = Integer.MAX_VALUE;
static boolean[][] map;
static int[][] v;
static Queue<Point> q = new ArrayDeque<>(1000001);
public static void main(String[] args) throws IOException {
st = new StringTokenizer(br.readLine());
n = stoi(st.nextToken());
m = stoi(st.nextToken());
map = new boolean[n][m];
v = new int[n][m];
for (int i = 0; i < n; i++) {
String s = br.readLine();
for (int j = 0; j < m; j++) {
map[i][j] = s.charAt(j) - '0' == 1;
}
}
//로직
bfs();
System.out.println(min == Integer.MAX_VALUE ? -1 : min);
}
static void bfs() {
q.add(new Point(0, 0, false, 1));
v[0][0] = COMPLETE_CHECK;
while (!q.isEmpty()) {
Point cur = q.poll();
if (cur.x == n - 1 && cur.y == m - 1) {
min = cur.cnt;
break;
}
for (int i = 0; i < 4; i++) {
int nx = cur.x + dx[i];
int ny = cur.y + dy[i];
//좌표를 벗어난 경우
if (nx < 0 || nx >= n || ny < 0 || ny >= m) continue;
//이미 방문한 경우
if (v[nx][ny] == COMPLETE_CHECK) continue;
// 벽을 뚫은 경로 이면서 이미 간 경우
if (v[nx][ny] == NONE_COMPLETE_CHECK && cur.isBroken) continue;
//벽이고 이미 한번 뚫은 경우는 뚫고가지 못한다.
if (map[nx][ny] && cur.isBroken) continue;
//벽이면서 뚫은 적이 없는 경우 뚫고간다.
if (map[nx][ny] && !cur.isBroken) {
v[nx][ny] = COMPLETE_CHECK;
q.add(new Point(nx, ny, true, cur.cnt + 1));
}
//벽이 아닌 경우 그냥 간다.
if (!map[nx][ny]) {
v[nx][ny] = cur.isBroken ? NONE_COMPLETE_CHECK : COMPLETE_CHECK;
q.add(new Point(nx, ny, cur.isBroken, cur.cnt + 1));
}
}
}
}
static int stoi(String s) {return Integer.parseInt(s);}
}
class Point{
int x;
int y;
boolean isBroken;
int cnt;
public Point(int x, int y, boolean isBroken, int cnt) {
this.x = x;
this.y = y;
this.isBroken = isBroken;
this.cnt = cnt;
}
}
기본적인 BFS에서 조금 응용된 문제였다.
다른 분들의 풀이를 보니 3차원 배열을 사용해서 풀이를 하셨다. 나는 3차원 배열은 쓸 생각은 하지도 못하고 2차원 방문 배열에 따로 상태를 두어 처리하였다.
기본적인 BFS의 방문처리는 단순히 true or false 로 나눠주면 된다.
하지만 이 문제의 경우에는 '벽을 한번 뚫고 지나가는 경로' 와 '벽을 한번도 뚫지 않은 경로' 가 있다.
만약에 해당 좌표(x, y), 건넌 횟수(cnt), 뚫고 지나갔는지 상태(isBroken)를 저장하는 Point 클래스를 만들어서 큐에 넣고 돌린다고 해도 단순히 방문 처리를 하게 된다면 먼저 뚫고 지나가는 경로에 의해 한번도 뚫지 못한 경로가 막히게 된다.
그렇기 때문에 벽을 뚫은 경로가 지나간 곳은 1로 처리를, 벽을 뚫지 않은 경로가 지나간 곳은 2로 처리를 해주었다.
그렇게 해야 뚫지 않은 경로가 지나갈 때 벽이 뚫은 경로를 무시하고 계속 나아갈 수 있다. 보통 벽을 먼저 뚫은 경로가 먼저 지나가기 때문이다.
'Dev > PS' 카테고리의 다른 글
[BOJ] 1581 락스타 락동호 java (0) | 2023.09.02 |
---|---|
[BOJ] 18430 무기 공학 java (0) | 2023.08.29 |
[BOJ] 1074 Z java (0) | 2023.02.07 |
[백준] 20056 마법사 상어와 파이어볼 java (0) | 2022.09.15 |
[백준] 17144 미세먼지 안녕! java (0) | 2022.08.18 |