매 단계마다 방문하지 않은 노드 중 최단 거리가 가장 짧은 노드를 선택하기 위해 (동작 과정 3) 1차원 테이블의 모든 원소를 확인 (=순차탐색) 한다
import sys
input = sys.stdin.readline
INF = int(1e9) # 무한을 의미하는 값으로 10억을 설정
#노드의 개수, 간선의 개수를 입력받기
n, m = map(int, input().split())
#시작 노드 번호를 입력받기
start = int(input())
#각 노드에 연결되어 있는 노드에 대한 정보를 담는 리스트를 만들기
graph = [[] for i in range(n + 1)]
#방문한 적이 있는지 체크하는 목적의 리스트 만들기
visted = [False] * (n + 1)
#최단 거리 테이블을 모두 무한으로 초기화
distance = [INF] * (n +1)
#모든 간선 정보를 입력받기
for _ in range(m):
a, b, c = map(int, input().split())
# a번 노드에서 b번 노드로 가는 비용이 c라는 의미
graph[a].append((b, c))
#방문하지 않은 노드 중에서, 가장 최단 거리가 짧은 노드의 번호를 반환
def get_smallest_node():
min_value = INF
index = 0 # 가장 최단 거리가 짧은 노드의 번호를 반환
for i in range(1, n + 1):
if distance[i] < min_value and not visted[i]:
min_value = distance[i]
index = i
return index
def dijkstra(strat):
#시작 노드에 대해서 초기화
distance[start] = 0
visted[start] = True
for j in graph[start]:
distance[j[0]] = j[1]
#시작 노드를 제외한 전체 n-1개의 노드에 대해 반복
for i in range(n - 1):
#현재 최단 거리가 가장 짧은 노드를 꺼내서, 방문 처리
now = get_smallest_node()
visted[now] = True
#현재 노드와 연결된 다른 노드를 확인
for j in graph[now]:
cost = distance[now] + j[1]
#현재 노드를 거쳐서 다른 노드로 이동하는 거리가 더 짧은 경우
if cost < distance[j[0]]:
distance[j[0]] = cost
dijkstra(start)
#모든 노드로 가기 위한 최단 거리를 출력
for i in range(1, n+1):
# 도달 할 수 없는 경우, 무한(INFINITY)이라고 출력
if distance[i] == INF:
print("INFINITY")
#도달할 수 있는 경우 거리를 출력
else:
print(distance[i])
위의 전체 코드에서 우리는 1차원 테이블의 모든 원소를 확인(= 순차탐색) 하기 위해 아래와 같은 코드(함수)를 부분적으로 사용하였고
def get_smallest_node():
min_value = INF
index = 0 # 가장 최단 거리가 짧은 노드의 번호를 반환
for i in range(1, n + 1):
if distance[i] < min_value and not visted[i]:
min_value = distance[i]
index = i
return index
이는 노드의 개수를 V라고 했을 때, 총 O(V)번에 걸쳐서 매번 선형 탐색을 하기 때문에
for i in range(n - 1): #O(V)
now = get_smallest_node() #O(V)
# ...(중략) ...
전체 시간 복잡도는 O(V^2)이다. ⛔️
일반적으로 1초에 2000만번 연산처리가 가능하면 합리적이므로, 노드 개수가 5000개 이하면 풀이가 가능하다. 하지만 노드의 개수가 10,000개를 넘어가는 문제라면 해당 코드로는 정답을 맞출 수 없다.
따라서 이러한 문제를 해결하기 위해 우선순위 큐를 사용한다.
우선순위 큐(Priority Queue)는 우선순위가 가장 높은 데이터를 가장 먼저 삭제하는 추상 자료형이다.
힙(Heap)은 우선순위 큐를 구현하는 데 사용하는 자료구조이다.
다음은 힙 정렬(최대힙, 최소힙)에 대한 예제 코드이다.
import heapq
def heapsort(iterable): # 오름차순
h = []
result = []
for value in iterable:
heapq.heappush(h, value)
for _ in range(len(h)):
result.append(heapq.heappop(h))
return result
def heapsort_des(iterable): # 내림차순
h = []
result = [] # 내림차순 정렬하여 받을 리스트
for value in iterable:
heapq.heappush(h, -value)
for _ in range(len(h)):
result.append(-heapq.heappop(h))
return result
print(heapsort([2, 5, 7, 2, 8, 4, 6, 8, 9]))
print(heapsort_des([2, 5, 7, 2, 8, 4, 6, 8, 9]))
힙 자료구조를 이용하여 성능을 개선해서 구현을 해보자. 구현 1과 원리는 완전히 똑같다. 다만,
매 단계마다 방문하지 않은 노드 중 최단 거리가 가장 짧은 노드를 선택하기 위해 (동작 과정 3)
1차원 테이블의 모든 원소를 확인 (=순차탐색) 하는 것이 아니라,(최소) 힙 자료구조를 사용한다.
import heapq
# sys.stdin.readline() 이란 파이썬 내장 함수로 input()을 치환하면,
# 입력 데이터 수가 많아도 빠르게 동작 가능하다.
import sys
input = sys.stdin.readline
INF = int(1e9) # 무한을 의미하는 값으로 10억을 설정
# 노드의 개수, 간선의 개수 입력받기
n, m = map(int, input().split())
# 시작 노드 번호를 입력받기
start = int(input())
# 각 노드에 연결된 노드에 대한 정보를 담는 리스트 생성
graph = [[] for i in range(n+1)]
# 최단 거리 테이블을 모두 무한으로 초기화
distance = [INF] * (n+1)
# 모든 간선 정보를 입력받기
for _ in range(m):
a,b,c = map(int, input().split())
# a번 노드에서 b번 노드로 가는 비용이 c
graph[a].append((b, c))
def dijkstra(start):
q = []
# 시작 노드로 가기 위한 최단 경로는 0으로 설정하여, 큐에 삽입
heapq.heappush(q, (0, start))
distance[start] = 0
while q: # 큐가 비어있지 않다면
# 최단 거리가 가장 짧은 노드에 대한 정보 꺼내기
dist, now = heqpq.heappop(q)
# 현재 노드가 이미 처리된 적이 있는 노드라면 무시 <- ⭐️ 다익스트라 알고리즘 특징 2
if distance[now] < dist:
continue
# 현재 노드와 연결된 다른 인접한 노드들을 확인
for i in graph[now]:
cost = dist + i[1]
# 현재 노드를 거쳐서, 다른 노드로 이동하는 거리가 더 짧은 경우
if cost < distance[i[0]]:
distance[i[0]] = cost
heapq.heappush(q, (cost, i[0]))
# 다익스트라 알고리즘을 수행
dijkstra(start)
# 모든 노드로 가기 위한 최단 거리를 출력
for i in range(1, n+1):
# 도달할 수 없는 경우, 무한(INFINITY)이라고 출력
if distance[i] == INF:
print(“INFINITY”)
# 도달할 수 있는 경우, 거리를 출력
else:
print(distance[i])
구현 - 1 코드와 달리 방문 여부를 판단하는 visited 테이블과 distance 테이블의 모든 원소를 확인하여 가장 최단 거리가 짧은 노드의 번호를 반환하는 get_smallest_node()가 없어지고, 다익스트라 함수 내에서 heapq.heappush()와 heapq.heappop()을 활용한 다는 것을 알 수 있다.
노드를 하나씩 꺼내 검사하는 반복문(while문)은 노드의 개수 V 이상의 횟수로는 처리되지 않는다. 또한 현재 우선순위 큐에서 꺼낸 연결된 다른 노드들을 확인하는 총 횟수는 최대 간선의 개수만큼 연산이 실행될 수 있습니다.
따라서 전체 시간복잡도는 O(ElogV) 이다. ✅
D(ab) = min(D(ab),D(ak)+D(kb))
INF=int(1e9)
n=int(input())
m=int(input())
graph=[[INF]*(n) for _ in range(n+1)]
for a in range(n): # 노드가 0 부터 n-1까지 있다고 가정
for b in range(n):
if a==b:
graph[a][b] = 0
for _ in range(m):
# A에서 B로 가는 비용은 C라고 설정
a, b, c = map(int,input().split())
graph[a][b] = c
for k in range(n):
for a in range(n):
for b in range(n):
graph[a][b] = min(graph[a][b], graph[a][k] + graph[k][b)
for a in range(n):
for b in range(n):
if graph[a][b] == INF:
print("INFINITE", end="")
else:
print(grpah[a][b], end="")
print()