[오답노트] 백준 #2206 벽 부수고 이동하기 (파이썬)

Yongjun Park·2022년 5월 2일
0

PS(Problem Solving)

목록 보기
21/31

오늘의 한 마디
와..

문제

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을 출력한다.

예제 입력 1

6 4
0100
1110
1000
0000
0111
0000

예제 출력 1

15

예제 입력 2

4 4
0111
1111
1111
1110

예제 출력 2

-1

발상

Try #1 : BFS 최단거리를 벽 개수만큼 시행

벽을 하나씩 없애보면서 각 경우에 대해 최단거리를 BFS로 구해보자!

import sys
INF = int(1e9)
DIRS = [(-1, 0), (1, 0), (0, -1), (0, 1)]

N, M = map(int, sys.stdin.readline().rstrip().split())
field = [list(map(int, list(sys.stdin.readline().rstrip()))) for _ in range(N)]

walls = []
for i in range(N):
    for j in range(M):
        if field[i][j]:
            walls.append((i, j))
           
def get_min_dist():
    visited = [[False]*M for _ in range(N)]
    q = [(0, 0)]
    visited[0][0] = True
    rv = 1
    while q:
        rv += 1
        q_cpy = q[:]
        q = []
        for y, x in q_cpy:
            for dy, dx in DIRS:
                if not (0 <= y+dy < N) or not (0 <= x+dx < M):
                    continue
                if field[y+dy][x+dx]:
                    continue
                if visited[y+dy][x+dx]:
                    continue
                if y+dy == N-1 and x+dx == M-1:
                    return rv
                visited[y+dy][x+dx] = True
                q.append((y+dy, x+dx))
    return INF

rv = INF
for y, x in walls:
    field[y][x] = 0
    rv = min(rv, get_min_dist())
    field[y][x] = 1
print(rv if rv != INF else -1)

실버 문제였다면 이렇게 풀렸겠지만,
난이도가 골드 4인 이유가 있겠죠?
시간 초과


풀이: 3차원 visited

아래의 풀이에서는 visited가 해당 점까지의 최단거리를 세이브하는 역할을 추가로 수행하기 때문에 변수명을 dist로 변경하였다.

값이 0일 때, not visited의 의미를 여전히 갖는다.

우선 코드부터 본 뒤에 중요한 부분을 설명하겠다.

import sys
from collections import deque
INF = int(1e9)
DIRS = [(-1, 0), (1, 0), (0, -1), (0, 1)]

N, M = map(int, sys.stdin.readline().rstrip().split())
field = [list(map(int, list(sys.stdin.readline().rstrip()))) for _ in range(N)]

def bfs():
    dist = [[[0]*2 for _ in range(M)] for _ in range(N)]
    dist[0][0][0] = 1 # default 거리 = 1
    q = deque([(0, 0, 0)])
    while q:
        y, x, used = q.popleft() # used -> 벽 파괴 찬스를 사용하고 낸 최단거리인지
        if y == N-1 and x == M-1:
            return dist[N-1][M-1][used]

        for dy, dx in DIRS:
            if not (0 <= y+dy < N) or not (0 <= x+dx < M):
                continue
            if field[y+dy][x+dx] and not used:
                dist[y+dy][x+dx][1] = dist[y][x][0] + 1
                q.append((y+dy, x+dx, 1))
            elif not field[y+dy][x+dx] and not dist[y+dy][x+dx][used]: # 한번도 해당 루트로 방문 안함
                dist[y+dy][x+dx][used] = dist[y][x][used] + 1 # 벽 파괴 찬스 사용 안했으면 계속 사용 안하고, 사용 이미 했으면 사용한 정보 연동
                q.append((y+dy, x+dx, used))
    return -1

print(bfs())

핵심은 bfs 한 번의 시행만으로도 벽 하나를 제거한 최단거리를 구할 수 있다는 점이다.

  • dist[0][0][0] : (0, 0) 점에서 벽 파괴 찬스를 안 이용하고 낸 최단거리
  • dist[4][2][1] : (4, 2) 점에서 벽 파괴 찬스를 이미 이용하고 낸 최단거리
            if field[y+dy][x+dx] and not used:
                dist[y+dy][x+dx][1] = dist[y][x][0] + 1
                q.append((y+dy, x+dx, 1))
            elif not field[y+dy][x+dx] and not dist[y+dy][x+dx][used]:
                dist[y+dy][x+dx][used] = dist[y][x][used] + 1
                q.append((y+dy, x+dx, used))

이게 핵심 파트다.

  • 현재 이동하려는 방향이 벽이지만, 아직 벽 파괴 찬스를 이용하지 않았다면,
    • 벽 파괴 찬스를 이용한다.
  • 현재 이동하려는 방향이 이동 가능하고, not visited라면,
    • 이동한다.
  • 이 외의 경우에 대해서는 이동하지 않고 삭제한다.

이 외의 경우 중 가장 흔한 케이스는, 이동 가능하긴 하지만 이미 dist 값이 있는 경우일 것이다.
이 경우는 비교도 해보지 않고 더 긴 거리라는 것을 보장할 수 있는데, 그 이유는...

어떤 조건문에서 이동하든지 dist는 1씩 증가하기 때문이다.
q에 들어가는 과정을 생각해보면, dist 값이 1인 경우가 다 해소되고 난 이후에 dist 값이 2인 경우를 고려하기 시작한다는 점을 알 수 있다.

따라서, 처음 dist 값이 입력되면 그게 그 점까지의 최단 거리다.

profile
추상화되었던 기술을 밑단까지 이해했을 때의 쾌감을 잊지 못합니다

0개의 댓글