해설 부분은 책에 내용을 포함하고, 더 추가하여 최대한 자세하게 설명하려고 노력했습니다.
문제 설명
어떤 나라에는 1 ~ N번까지의 도시와 M개의 단방향 도로가 존재합니다. 모든 도로의 거리는 1입니다. 이때 특정한 도시 X로부터 출발하여 도달할 수 있는 모든 도시 중에서, 최단 거리가 정확히 K인 모든 도시의 번호를 출력하는 프로그램을 작성하세요. 또한 출발 도시 X에서 출발 도시 X로 가는 최단 거리는 항상 0이라고 가정합니다.
예를 들어 N = 4, K = 2, X = 1 일 때 다음과 같이 그래프가 구성되어 있다고 합시다. 이때 1번 도시에서 출발하여 도달할 수 있는 도시 중에서, 최단 거리가 2인 도시는 4번 도시뿐입니다. 2번과 3번 도시의 경우, 최단 거리가 1이기 때문에 출력하지 않습니다.
입력 조건
- 첫째 줄에 도시의 개수 N, 도로의 개수 M, 거리 정보 K, 출발 도시의 번호 X가 주어집니다. (2 <= N <= 300,000, 1 <= M <= 300,000, 1 <= K <= 300,000, 1 <= X <= N)
- 둘째 줄부터 M개의 줄에 걸쳐서 두 개의 자연수 A, B가 주어지며, 각 자연수는 공백으로 구분합니다. 이는 A번 도시에서 B번 도시로 이동하는 단방향 도로가 존재한다는 의미입니다. (1 <= A, B <= N) 단, A와 B는 서로 다른 자연수입니다.
출력 조건
- X로부터 출발하여 도달할 수 있는 도시 중에서, 최단 거리가 K인 모든 도시의 번호를 한 줄에 하나씩 오름차순으로 출력합니다.
- 이때 도달할 수 있는 도시 중에서, 최단 거리가 K인 도시가 하나도 존재하지 않으면 -1을 출력합니다.
입력 예시 1
4 4 2 1
1 2
1 3
2 3
2 4
출력 예시 1
4
입력 예시 2
4 3 2 1
1 2
1 3
1 4
출력 예시 2
-1
입력 예시 3
4 4 1 1
1 2
1 3
2 3
2 4
출력 예시 3
2
3
문제 해설
BFS의 개념을 가장 잘 나타내는 문제라고 생각한다. 그래서 책의 저자도 이 문제를 첫 번째에 위치시킨 거라고 믿는다. 앞으로도 여러 번 반복해서 체화시키겠다.
그럼 해설을 시작하겠다.
그래프에서 모든 간선의 길이가 동일하면 BFS를 이용해서 문제를 해결할 수 있다. 왜냐하면 BFS는 '너비 우선 탐색'이다. 가장 가까운 위치 먼저 탐색하고 그 다음으로 넘어가는 방식인데, BFS는 큐를 이용해서 구현할 수 있다. 특정한 배열에 데이터가 선입선출되는 과정이 큐의 구조이기 때문이다. 스택과 큐의 차이에 대해서는 후에 블로그에 정리하는 글을 쓰겠다.
이 문제에서 주목할 점은 입력 값 중에서 M의 범위이다. 범위가 최대 100만인데 백만이면 O(N)으로 해도 충분히 시간을 을 통과할 수 있을거라고 생각한다.
여담으로, 최단 거리가 K와 일치하는 도시를 구하는 문제인데, 그래프를 행렬로 표현할 수 있고 행렬 Ak = aij가 k번 만에 aij로 갈 수 있는 경로의 수라는 선형대수학 과목에서 배운 내용도 생각나서 좋았다.
책에서 제시하는 해설 코드는 다음과 같다.
from collections import deque
n, m, k, x = map(int, input().split())
# 그래프로 노드의 개수 정해줌
graph = [ [] for _ in range(n + 1) ]
# 연결된 노드를 저장하는 방법
for _ in range(m):
a, b = map(int, input().split())
graph[a].append(b)
# 모든 도시에 대한 최단 거리 초기화
distance = [-1] * (n + 1)
distance[x] = 0 # 출발 도시까지의 거리는 0
# bfs 수행
queue = deque([x]) # 리스트니까 [x]로 시작
while queue:
now = queue.popleft()
# 현재 도시에서 이동할 수 있는 모든 도시를 확인
for next_node in graph[now]:
# 아직 방문하지 않은 도시라면
# bfs는 너비 우선 탐색이라 가까운것부터 탐색 따라서 처음에 방문이 최단거리를 보장
if distance[next_node] == -1:
# 최단 거리 갱신
distance[next_node] = distance[now] + 1
queue.append(next_node)
# 여기까지 했으면 갈 수 있는 모든 노드의 최단거리 지정완료
# 최단 거리가 k인 모든 도시의 번호를 오름차순으로 출력
check = False
for i in range(1, n + 1):
if distance[i] == k:
print(i) # 한 줄에 하나씩 오름차순으로 출력해야 하니까
check = True
# 만약 최단 거리가 k인 도시가 없다면, -1 출력
if check == False:
print(-1)
행렬을 입력할 배열 graph를 넉넉하게 n+1개 까지 만들어 준다.
리스트 컴프리핸션을 이용했는데 파이썬 리스트 컴프리핸션도 후에 블로그에 올리겠다.
이제 시간 복잡도를 고민하게한 간선의 연결을 입력받을 차례인데 반복문으로 m만큼 돌려준다. 그리고 행렬을 표현한 배열 graph에 graph[a].append(b) 형태로 입력값을 넣는다. 처음 볼 때 이 부분에서 아이디어를 떠올리기 어려웠다.
그리고 출력해야 할 노드를 거리값으로 비교하니 거리 값 distance를 -1로 넉넉하게 n + 1개 초기화해준다. 그리고 출발지점은 distance[x] = 0으로 거리를 맞춰준다.
queue = deque([x]) 이 부분이 이해가 안됐는데, 선입선출(FIFO)의 구조를 가진 배열을(처음 들어간 데이터는 왼쪽에 저장) 괄호 안에 배열로 초기화한다는 의미이다.
즉, queue를 배열로 선언하는데 안에 출발지점을 나타내는 데이터 x 하나만 넣겠다는 의미이다.
while queue: 는 queue배열이 텅 비어질 때까지 반복하겠다는 뜻이다.
now = queue.popleft() 는 배열 queue의 가장 왼쪽에 들어간 데이터를 꺼내서 now에 초기화하겠다는 것이다.
반복문을 진행하면서 now는 계속 바뀌게 되는데 마지막 줄에 queue.append(next_node)가 있어서, 두번째 now는 출발지점 x와 가장 먼저 연결된 노드가 된다.
그렇게 마지막 노드까지 연결지점을 다 순환하고 더 append할게 없을 때까지 도는 것이다.
for next_node in graph[now]:
if distance[next_node] == -1:
distance[next_node] = distance[now] + 1
queue.append(next_node)
중요
반복문을 돌면서 if distance[next_node] == -1: 인 조건문이 나온다. 그리고 조건문을 만족해야지만 거리를 갱신하고 queue에 새로운 노드를 넣어준다.
왜 거리가 -1일때만 즉, 처음으로 접근할 때만 값을 갱신하는 걸까?
처음이 아닌 다음번 순환에서 거리의 최솟값을 갱신하라 수 있지 않을까?
하지만 bfs의 성질상 그렇지 않다.
모든 거리값을 -1로 초기화하고 출발지점부터 시작해서 그 주변을 하나씩 방문해서 탐색하는 bfs방식은 처음 입력값이 최솟값을 보장한다.
예를 들어 노드 2를 향해 가는 거리는 1의 거리에서 한칸을 움직여서 2로 도착할 수 있다. 그러면 거리의 최솟값은 2가 된다.
그리고 3을 가는 경우는 (노드 3이 1과 2에 모두 간선이 있다고 할 때) 1에서 바로 3으로 가는 한 칸 움직이는 경우와 1에서 2를 거쳐 3으로 가는 두 칸을 움직이는 경우가 있다.
무엇이 '최소경로'인가?
1에서 3으로 한칸 움직이는 경우가 최소의 경로이다.
1 -> 3 인 것이다.
1 -> 2 -> 3인 경로는 최소 경로가 아닌 것이다.
그리고 코드는 최소경로를 훌륭하게 판별해 주고 있다.
바로 큐의 성질을 이용해서 말이다.
출발지점인 x를 1로 설정했었다. 그리고 노드 1과 연결된 간선을 가진 노드는 2와 3이라고 했다. 그리고 노드 2와 3도 간선이 있다.
now = queue.popleft() 를 통해 지금의 now는 1이다. graph[1]에서 나오는 데이터 즉, 간선은 2와 3이다.
그렇다면 2가 먼저 next_node가 될것이다.
distance[2] 는 -1일 테니 조건문을 만족한다. 그리고 거리를 갱신해 distance[2]는 값이 1이 된다.
그리고 queue.append(next_node)를 통해 2를 다음 데이터로써 queue에 넣는다.
자 이제 2는 끝났다. 다음 next_node는 3이다.
3도 처음 방문이다. 그래서 distance[3]도 값이 -1이다.
그래서 갱신해 준다. 앞선 2와 똑같은 과정을 거쳐 값이 1이 된다.
그리고 queue.append(3)을 수행한다.
이제 now가 1인 경우는 모두 끝났다.
그러면 while문이 한번 돌아 now = 2가 된다.
앞선 1은 queue에서 버려졌기 때문이다.
graph[2]에서 내부의 데이터를 next_node라는 반복 변수로 꺼내오는데 노드 2는 노드 3과 간선(edge)이 있어서 next_node로 노드 3을 불러온다.
드디어 궁금했던 노드 3의 두번째 호출이다!!
하지만 이미 위에서 설명했다. 노드 2에서 노드 3을 가는 경우는 세밀하게 말하면 노드 1을 먼저 거쳐서 가는 경우이다.
즉, 1 -> 2 -> 3 인것이다.
그렇다면 두칸을 넘어야 하는 셈이 되고 이것은 우리가 찾던 '최소 경로'가 아니게 된다.
이것을 방지하기 위해 if distance[next_node] == -1: 을 걸어놨던 것이다.
어차피 출발지점 1에서 누적되서 올라가는 경우이니 처음 방문만이 '최소 경로'를 보장해 주기 때문이다.
조건을 걸지 않으면 최소 경로가 아닌 최대 경로도 아닌 들쑥날쑥한 값이 나오게 될 것이다.
https://www.acmicpc.net/problem/18352
'[Coding Test] > [이코테]' 카테고리의 다른 글
숫자 카드 게임(그리디) - 이코테(p.96) (0) | 2022.05.12 |
---|---|
큰 수의 법칙(p.92) - 그리디(이코테) (0) | 2022.05.12 |
두 배열의 원소 교체(p.182) - 정렬(이코테) (0) | 2022.03.26 |
성적이 낮은 순서로 학생 출력하기(p.180) - 정렬(이코테) (0) | 2022.03.26 |
위에서 아래로(p.178) - 정렬(이코테) (0) | 2022.03.26 |