이번 주에 풀었던 DFS/BFS 문제 시리즈는 모두 너무 어려웠고 지금 내가 할 수 없는 것에 너무 시간을 낭비하는 게 아닌가(? 물론 고민을 해봤다는 것은 지금 당장은 도움도 안 되고 기억에도 남지 않는 것 같아도 조금씩 쌓여서 최소한 사고력을 키우는 데 도움이 될 것이니 그렇게 조급할 필요는 없을 것이다.) 계속해도 될 것인가 고민이 많았던 게 사실이지만 열심히 고생한 결과 이 문제의 합격이 떠서 기쁘다. 그리고 정말 피곤하다. ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ
https://www.acmicpc.net/problem/2178
미로 탐색 문제를 처음에 봤을 때에는 도대체 이런 문제를 일주일 안에 풀 수 있을지에 대해 답답한 마음뿐이었는데 여러 가지 BFS 문제를 지금까지 풀어보고 나니 이 문제는 근본적으로 처음 풀어본 BFS 문제인 1697번 숨바꼭질과 거의 같은 구성으로 되어 있다. 물론 지난 주말만 해도 나는 이 문제나 그 문제나 왜 BFS인지 자체를 이해하지 못하고 있었지만! ㅋㅋㅋ
왜 최단거리 찾기가 BFS일까?
그래프나 트리나 아무튼 하나의 시작점에서 2,3,4,5,6...이라는 점으로 선이 뻗어나가 있다면
DFS는 2로 먼저 간 다음에 2에서 뻗어나간 점 11,12,13,14,15... 중에서 또 11을 찍고 11에서 또 101,102,103,104...가 뻗어나가고 있다면 또 101을 찍고... 이런 식으로 나간다.
반면에 BFS는 시작점에서 직접 연결된 점 2,3,4,5,6,7,8,9,10을 찍고
그 다음에 2에서 연결된 점을 다 찍고 3에서 연결된 점을 다 찍고 4에서 연결된 점을 다 찍고... 이렇게 탐색한다.
그러니까 시작점에서 거리가 1인 점을 다 뽑고, 그 다음에 거리가 1인 점들에서 직접 연결된 점, 즉 총 거리가 2인 점을 다 뽑고... 이런 순서로 찾아나가기 때문에 최초로 해당 도착점이 발견되는 시점이 그 도착점까지의 최단거리인 셈이다.
만약에 조사하려는 자료구조가 트리가 아니고 사이클이 있는 그래프라서 방문할 수 있는 경로가 여러 가지가 있다고 하더라도 먼저 찾은 경로가 더 적은 정점을 방문한 시점이기 때문에 특정 정점에 대해서는 먼저 찾은 거리가 최단 거리인 것 같다.
이처럼 처음에 1697번 숨바꼭질 문제를 풀 때에는 왜 BFS이며 BFS는 왜 그런 방식으로 탐색하는지 전혀 이해가 안 갔지만 이제는 할 수 있어서 보람을 느낀다.
본론으로 들어가서 문제를 풀어보려고 하는데 이 문제는 일단 입력 구조가 좀 헷갈린다.
첫째 줄에 두 정수 N, M(2 ≤ N, M ≤ 100)이 주어진다. 다음 N개의 줄에는 M개의 정수로 미로가 주어진다. 각각의 수들은 붙어서 입력으로 주어진다.
왜 M,N이 아니고 N,M인지 모르겠는데 일단 이것 때문에 은근히 헷갈렸고 오타가 많이 발생할 뻔했다는 점을 일단 기록해 두고... ㅋㅋㅋ
두 개의 정수를 공백으로 구분하면서 입력하려면 일단 이렇게 입력한다.
import sys
N,M=map(int,sys.stdin.readline().split())
그리고 지금까지 백준 온라인 저지 문제에서 대부분 한 줄에 여러 값을 입력받으려면 공백으로 구분했는데 이번에는 붙어서 나간다. 예를 들어서 M=7
이라면 하나의 7칸의 미로 구성은 1100111
이런 식으로 입력할 수 있다.
일단 하나의 문자열을 입력받아서 그것을 한 글자씩으로 이루어진 배열로 만들려면 list(input())
이라고 하는 방법이 있다는 것을 이번 주에 처음 검색해보고 알았는데...
이 문제의 테스트 케이스 조건을 보면 한 줄에 최대 100글자씩 최대 100줄을 입력하는 과정 자체에서 파이썬에서 시간초과가 뜰 가능성이 높으니까 최대한 더 처리속도가 빠른 걸로 해보도록 해보면...
maze0 = list(map(int,list(sys.stdin.readline().strip())))
이런 식으로 할 수 있다고 한다. 뒤의 strip()
은 왜 쓰냐면 라인을 통째로 입력받았을 때 마지막의 엔터키 입력도 포함할 수가 있기 때문에 앞뒤의 공백이나 널문자를 다 빼고 처리하도록 하는 것이라고 한다.
그리고 M칸의 미로를 N줄 입력 받으니까(아, 이렇게 한국말로 풀어서 썼을 때에 자연스러우니까 처음에 입력받을 때 M과 N을 입력받는 게 아니라 N과 M을 입력받는다고 했나? ㅋㅋㅋ) 이 과정을 N회 반복해서 하나의 2차원 배열 안에 다 넣어야 한다.
maze=[list(map(int,list(sys.stdin.readline().strip()))) for x in range(N)]
그리고 BFS는 한번 방문한 곳은 다시 방문하지 않기 때문에 방문 여부를 표시할 동일한 크기의 2차원 배열을 하나 더 만들어 놓고 초깃값은 모두 0으로 한다.
visited=[[0]*(M) for x in range(N)]
또한 각각의 칸의 최단거리를 측정하고 그것을 바탕으로 최단거리가 이미 측정된 칸에서 인접한 칸의 최단거리는 +1을 해주는 방법이기 때문에 각각의 칸의 현재 최단거리를 입력할 동일한 크기의 2차원 배열을 하나 더 만든다. 초깃값은 현재 구하지 않았다면 '무한대'이겠지만 이 문제의 입력값 조건이 가로 100 세로 100이 최대이기 때문에 모든 칸을 다 방문하더라도(물론 모든 칸이 다 방문할 수 있도록 0이 없이 모두 1로 개방되어 있다면 다른 지름길도 있겠지만 ㅋㅋㅋ) 도착점까지의 거리는 10000이 최대니까 그것보다 큰 값으로 정해 두었다. 물론 그냥 0
으로 채워두고 방문여부는 앞의 visited[]
배열에서 판정해도 된다.
shortest = [[10001]*(M) for x in range(N)]
현재 최단거리를 업데이트하는 2차원 배열을 따로 만드는 이유는 각각의 정점의 최단거리는 이미 최단거리를 측정한 인접한 정점의 최단거리에 +1을 하는 방식으로 계산하기 때문이다.
예를 들어서 시작점의 최단거리는 1이기 때문에(현재 문제에 제시된 테스트 케이스와 정답을 보니 (0,0)
의 최단거리를 0이 아니라 1이라고 간주하고 있다.) 시작점 바로 오른쪽 점 (0,1)
의 최단거리는 (0,0)
에 인접한 점이라는 사실을 바탕으로 1+1=2
라고 하면 된다. 이렇게 인접한 점의 최단거리를 확정함으로써 (0,0) →(1,0) →(1,1) →(0,1) 같은 최단거리가 아닌 경로는 무시할 수 있다.
이제 여기에서 BFS는 어떻게 수행하는가?
물론 시작점 (0,0)
에서 도착점 (N-1,M-1)
까지 모두 구할 때까지 하면 된다.
[[0,0]]
일 것이다. 시작점의 최단거리는 1로 초깃값이 정해져 있다.그리고 마지막으로 shortest[N-1][M-1]
에 저장된 값을 출력해주면 도착점의 최단거리를 출력하라는 문제의 풀이가 끝난다.
# 백준 2178번 미로 탐색
import collections
import sys
N,M=map(int,sys.stdin.readline().split())
#houses = [list(map(int,list(input()))) for x in range(N)]
maze=[list(map(int,list(sys.stdin.readline().strip()))) for x in range(N)]
#print(maze)
visited=[[0]*(M) for x in range(N)]
#print(visited)
shortest = [[10001]*(M) for x in range(N)]
to_search = collections.deque([[0,0]])
comparison=[[-1,0],[1,0],[0,-1],[0,1]]
shortest[0][0]=1
while to_search:
#print(to_search)
current_loc = to_search.popleft()
if visited[current_loc[0]][current_loc[1]]==0:
visited[current_loc[0]][current_loc[1]]=1
for compare in comparison:
compare=[current_loc[0]+compare[0],current_loc[1]+compare[1]]
#print(compare)
if compare[0]>=0 and compare[0]<N \
and compare[1]>=0 and compare[1]<M \
and maze[compare[0]][compare[1]]==1 \
and visited[compare[0]][compare[1]]==0:
to_search.append(compare)
visited[compare[0]][compare[1]]=1
shortest[compare[0]][compare[1]]=shortest[current_loc[0]][current_loc[1]]+1
#print(shortest)
print(shortest[N-1][M-1])
지금까지 푼 다른 BFS 문제들은 정말 어려웠지만 그 문제들을 바탕으로 BFS에 대한 이해를 하고 나니 별로 막힘이 없이 잘 풀렸다.
그런데 앞의 두 문제도 코드가 거의 똑같은 것 같은데 왜 시간 초과가 뜰까? ㅠㅠ ㅋㅋㅋㅋㅋㅋㅋ 그런데 이미 여기까지 해보는 것만 해도 너무 시간이 오래 걸렸기 때문에 다음에 다시 해봐야겠다. ㅋㅋㅋ
얼마만에 한 번에 합격하는 건지 모르겠다 ㅋㅋㅋ
그리고 파이썬에서 여러 값을 한 줄에 입력받는 방법을 다음에도 쓸 수 있게 기억해두어야겠다.
import sys
N,M=map(int,sys.stdin.readline().split())
#houses = [list(map(int,list(input()))) for x in range(N)]
maze=[list(map(int,list(sys.stdin.readline().strip()))) for x in range(N)]