import java.util.Scanner;
import java.util.Queue;
import java.util.LinkedList;
// 2206 벽 부수고 이동하기
public class Main {
static int N, M, cnt;
static int[][] map;
static boolean visited[][][];
static Queue<Pair> queue;
static int[][] temp = { { 1, 0 }, { 0, 1 }, { -1, 0 }, { 0, -1 } };
static boolean flag;
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
N = sc.nextInt();
M = sc.nextInt();
map = new int[N][M];
visited = new boolean[N][M][2];
for (int i = 0; i < N; i++) {
String st = sc.next();
for (int j = 0; j < M; j++) {
map[i][j] = st.charAt(j) - 48;
}
}
visited[0][0][1] = true;
BFS(0, 0, 1);
if(flag == true) System.out.println(cnt);
else System.out.println("-1");
}
public static void BFS(int x, int y, int drill) {
queue = new LinkedList<Pair>();
queue.offer(new Pair(x, y, drill));
visited[x][y][drill] = true;
Outter : while (!queue.isEmpty()) {
int size = queue.size();
for (int j = 0; j < size; j++) {
Pair current = queue.poll();
if(current.x == N-1 && current.y == M-1) {
cnt++;
flag = true;
break Outter;
}
for (int i = 0; i < 4; i++) {
int nextX = current.x + temp[i][0];
int nextY = current.y + temp[i][1];
if (!isInside(nextX, nextY))
continue;
// 벽면을 만나면
if (map[nextX][nextY] == 1) {
// 드릴을 가지고 있고, 드릴을 사용하지 않고 방문하지 않은것이 확인되었으면
if (current.drill == 1 && visited[nextX][nextY][0] == false) {
visited[nextX][nextY][0] = true;
queue.add(new Pair(nextX, nextY, 0));
}
} else {
// 이동할 수 있는 곳을 만나면
if (visited[nextX][nextY][current.drill] == false) {
visited[nextX][nextY][current.drill] = true;
queue.add(new Pair(nextX, nextY, current.drill));
}
}
}
}
cnt++;
}
}
public static boolean isInside(int x, int y) {
if (x >= 0 && x < N && y >= 0 && y < M)
return true;
else
return false;
}
}
class Pair {
int x;
int y;
int drill;
Pair(int x, int y, int drill) {
this.x = x;
this.y = y;
this.drill = drill;
}
}
최단거리를 구하는 문제이기 때문에 BFS로 구할 수 있다. BFS로 최단경로를 구하는 것은 간단하지만, 이 문제에서는 한가지 조건이 추가 된다. 이동하는 도중에 만나는 한개의 벽을 부술 수 있다는 것인데, 내가 처음에 생각 했던 생각은 입력을 받고 모든 1을 한번씩 빼면서 BFS를 하는 것 이었다. 하지만 그렇게 하면 되면 너무 비효율적이고 N, M의 입력이 1000까지 이었기 때문에 당연히 시간초과가 나왔다.
문제해결의 핵심 첫 번째는 방문하는 visited 배열을 3차원으로 선언하여 벽을 뚫고 온 길인지, 아닌지를 판단하는 것이다. 두 번째는 Pair 클래스의 x, y좌표 말고도 drill 변수를 추가로 두는 것이다. drill이 1이면 벽을 뚫 수 있는 drill 을 한개 가지고 있다는 뜻이고, 0이면 드릴을 썻기 때문에 더 이상 벽을 뚫 수 없다는 뜻이다.
그리고 또 하나 중요한 점은 최단거리를 어떻게 구하냐는지 이다. 구글링을 하다보니까 여기에서 BFS하면서 최단거리를 구하는 새로운 스킬? 같은 것을 처음 배울 수 있었는데, queue에 탐색할 좌표를 넣고 pop을 할 때 queue의 size만큼 for문을 반복하는 것이다. queue의 size만큼 for문을 반복하고 나온 후에 cnt 변수를 1씩 증가시키면 queue 안에 있던 탐색 가능한 좌표들이 한번 움직일 때 마다 거리를 몇만큼 움직이는지 체크하여 N-1, M-1 좌표에 도달 했을 때 최단 거리를 cnt변수로 출력할 수 있다. 이 부분은 queue의 size만큼 반복하는 for문을 보면 쉽게 이해 할 것이다. 처음 알게된 스킬?인만큼 잘 숙지해야겠다.
< 문제 풀이 >
1. 처음 좌표인 0, 0에서 BFS를 시작하는데 드릴을 가지고 있으므로 BFS(0, 0, 1)을 처음 시작한다.
2. 그러면 queue에 (0,0,1) 이 들어가고, 주어진 첫번 째 예제를 예로들어 설명해보면 0,0 에서 갈 수 있는 방향은 0,1 과 1,0 이다. 하지만 둘다 벽이므로 갈 수 있는 모든 방향을 벽을 뚫어야 한다. 그러므로 0,0,1 을 pop하면서 좌표 1,0과 0,1 을 queue에 push 하는데 queue에 들어갈 좌표는 결론적으로 두가지를 확인해야 한다.
① 벽면을 만났는데, 현재까지 드릴을 사용하지 않고 왔고, visited[nextX][nextY][0] == false 인지 확인해야 한다. visited[nextX][nextY][0] 이 뜻하는 것은 "벽면을 뚫지 않고 왔던 길이냐" 하는 것이다. 벽면을 뚫지 않고 온 길이여야지 벽면을 뚫 수 있기 때문이다. 만약에 ①번 조건을 만족시키면 드릴을 썻기 때문에 queue에 좌표와 드릴을 썻다는 의미로 0을 push하고 visited[nextX][nextY][0] 을 true 로 만든다. 이제 방금들어간 Pair는 드릴을 썻기 때문에 앞으로 좌표를 탐색할 때 벽면을 만나면 지나가지 못할 것이다.
② 다음으로 이동할 수 있는 곳을 만나면 드릴이 있던 없던 지나갈 수 있으므로 visited 배열의 방문 표시만 해주고 지나가면 된다.
이렇게 queue 에 어떤 좌표를 넣을지를 판단하고
3. 마지막에 for 문을 빠져나오면 cnt 을 1 증가시켜 0,0 좌표에서 현재 위치까지 좌표를 업데이트 해준다.
4. queue에서 pop 한 좌표가 N-1, M-1 이면 BFS를 멈춘다. 여기에서 cnt를 왜 1증가시키냐면 처음 좌표를 0으로 잡아서 마지막에 1을 증가시켜야 답이 나오기 때문이다. 그리고 boolean 타입의 flag가 뜻하는 것은 목표 지점에 도달했느냐 안했느냐 이다. 목표 좌표인 N-1, M-1에 도달 했으면 true로 바꿔서 cnt를 출력해주고, 도착하지 못했으면 false 상태이므로 벽면에 막혀서 도달했지 못했다 라는 뜻이기 때문에 -1을 출력한다.
'알고리즘 > 문제풀이' 카테고리의 다른 글
[5430] AC (0) | 2019.04.09 |
---|---|
[1182] 부분수열의 합 (0) | 2019.04.03 |
[6603] 로또 (0) | 2019.04.02 |
[7562] 나이트의 이동 (0) | 2019.04.02 |
[2293] 동전 (0) | 2019.03.31 |