[알고리즘] 다익스트라 알고리즘 (Dijkstra Algorithm)

박지훈·2021년 4월 21일
1

Algorithm

목록 보기
8/13

최단 경로 알고리즘

최단 경로 알고리즘은 말 그대로 가장 짧은 경로를 찾는 알고리즘이다. ('길 찾기 문제' 라고도 불린다.) 최단 경로 알고리즘 유형에는 다양한 종류가 있다. 대표적으로 다익스트라 최단 경로 알고리즘, 플로이드 워셜, 벨만 포드 알고리즘, 이렇게 3가지이다. 그중 다익스트라 알고리즘에 대해 정복해보도록 하자.

  • <최단 경로 구해야하는 문제 상황의 경우들>

    • 한 지점에서 다른 특정 지점까지의 최단 경로
    • 모든 지점에서 다른 모든 지점까지의 최단 경로
    • 한 지점에서 다른 모든 지점까지의 최단 경로
  • 각 지점은 그래프에서 노드로 표현

  • 지점 간 연결된 도로는 그래프에서 간선으로 표현


다익스트라 알고리즘 (Dijkstra Algorithm)

  • 다익스트라 알고리즘은 그래프에서 여러 개의 노드가 있을 때, 특정한 노드에서 출발하여 다른 노드로 가는 각각의 최단 경로를 구해주는 알고리즘

  • 다익스트라는 매번 '가장 비용이 적은 노드'를 선택해서 임의의 과정을 반복하는데, 이 때문에 기본적으로 그리디 알고리즘으로 분류된다.

  • 다익스트라 알고리즘의 원리

    1. 출발 노드를 설정
    2. 최단 거리 테이블을 초기화
    3. 방문하지 않는 노드 중에서 최단 거리가 가장 짧은 노드를 선택
    4. 해당 노드를 거쳐 다른 노드로 가는 비용을 계산하여 최단 거리 테이블 갱신
    5. 3, 4번 과정 반복

최단 경로를 구하는 과정에서 최단 거리 테이블은 '각 노드에 대한 현재까지의 최단 거리 정보'를 항상 1차원 리스트에 저장하며 리스트를 계속 갱신하는 특징이 있다. 처리 과정에서 더 짧은 경로를 찾으면 '이제부터는 이 경로가 제일 짧은 경로야'라고 갱신한다.

그리디 알고리즘과 DP 알고리즘은 최단 경로 알고리즘에 그대로 적용된다는 특징이 있다. 다익스트라 알고리즘은 사실 그리디 알고리즘 및 DP 알고리즘의 한 유형으로 볼 수 있다!

동작 과정 그림으로 보기

[step 0] 그래프에서 시작 노드를 설정


[step 1] 1번 노드를 시작으로 1번 노드와 연결되었고 방문하지 않은 노드를 처리


[step 2] 1번 노드와 연결된 노드 중 최단 거리가 가장 짧은 4번 노드를 방문하고, 4번 노드와 연결된 3번과 5번 노드의 거리 갱신


[step 3] 4번 노드가 처리됬으면 그 다음 최단 거리인 2번 노드를 처리한다. 이때 2번 노드와 연결된 3번 노드도 처리될텐데 1 -> 2 -> 3의 비용인 5보다 1 -> 4 -> 3의 비용이 작기 때문에 갱신되지 않는다. (최단 거리를 구하는 것이므로 무시된다.)


[step 4] 2번 노드가 처리됬으면 그 다음 최단 거리인 5번 노드를 처리한다. 5번 노드와 연결된 3번, 6번 노드의 거리 갱신


[step 5] 그 다음 최단 거리인 3번 노드를 처리


[step 6] 그 다음 최단 거리인 6번 노드를 처리


간단한 다익스트라 알고리즘 코드 (Python)

# ★ 단계마다 방문하지 않은 노드 중에서 최단 거리가 가장 짧은 노드를 선택하기 위해
# 매 단계마다 1차원 테이블의 모든 원소를 확인(순차 탐색)한다.
import sys

input = sys.stdin.readline
INF = int(1e9)
n, m = map(int, input().split())
# 시작 노드 번호
start = int(input())

graph = [[] for _ in range(n + 1)]
visited = [False] * (n + 1)
distance = [INF] * (n + 1)  # 최단거리 테이블

for _ in range(m):
    # a번 노드 -> b번 노드로 가는 비용이 c라는 의미
    a, b, c = map(int, input().split())
    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 visited[i]:
            min_value = distance[i]
            index = i

    return index


def dijkstra(start):
    distance[start] = 0
    visited[start] = True
    # 시작 노드와 연결된 정점들의 경로 갱신
    for j in graph[start]:
        distance[j[0]] = j[1]

    # 시작 노드를 제외한 전체 n - 1개의 노드에 대해 반복
    for i in range(n - 1):
        now = get_smallest_node()
        visited[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):
    if distance[i] == INF:
        print("INFINITY")
    # 최단 경로 출력
    else:
        print(distance[i])

# sample input
# 6 11
# 1
# 1 2 2
# 1 3 5
# 1 4 1
# 2 3 3
# 2 4 2
# 3 2 3
# 3 6 5
# 4 3 3
# 4 5 1
# 5 3 1
# 5 6 2

단계를 거치면서 한 번 처리된 노드의 최단 거리는 고정되어 더 이상 바뀌지 않는다. 왜냐하면 최단 거리보다 큰 거리로 갱신되면 이는 최단 거리가 아니기 때문이다. 즉, 한 단계당 하나의 노드에 대한 최단 거리를 확실히 찾은 것이라고 이해하면 된다.

왜 1차원 리스트에는 '최단 거리'만을 저장하고 있는지 궁금할 수 있다. 완벽한 형태의 최단 경로를 구하려면 소스코드에 조금 수정해야한다. 그러나 보통 코딩 테스테에서는 대체로 특정한 노드에서 다른 특정한 노드까지의 최단 거리만을 출력하도록 요청하므로, 추후에 포스팅 하겠다.

  • 시간 복잡도는 O(V^2)
    O(V)번에 걸쳐서 최단 거리가 가장 짧은 노드를 매번 선형 탐색해야 하고, 현재 노드와 연결된 노드를 매번 일일이 확인하기 때문이다.
    코딩 테스트의 최단 경로 문제에서 전체 노드의 개수가 5,000개 이하이면 일반적으로 풀 수 있다. 하지만 노드의 개수가 10,000개를 넘어가는 문제라면 위 코드로 풀 수 없다. 이를 해결하려면 '개선된 다익스트라 알고리즘' 을 사용해야 한다.


개선된 다익스트라 알고리즘 그림으로 보기

[step 0] 그래프에서 시작 노드를 설정하고 우선순위 큐에 넣는다.


[step 1] 우선순위 큐에서 첫 번째 원소를 꺼낸다. 1번 노드는 방문한 적이 없으므로 1번 노드를 처리


[step 2] 우선순위 큐에서 4번 노드(위 그림의 (1,4)인 원소)를 pop 한다. 4번 노드는 방문한 적이 없으므로 4번 노드를 처리


[step 3] 우선순위 큐에서 2번 노드(위 그림의 (2,2)인 원소)를 pop 한다. 2번 노드는 방문한 적이 없으므로 2번 노드를 처리


[step 4] 우선순위 큐에서 5번 노드(위 그림의 (2,5)인 원소)를 pop 한다. 5번 노드는 방문한 적이 없으므로 5번 노드를 처리


[step 5] 우선순위 큐에서 3번 노드(위 그림의 (3,3)인 원소)를 pop 한다. 3번 노드는 방문한 적이 없으므로 3번 노드를 처리


[step 6] 우선순위 큐에서 3번 노드(위 그림의 (4,3)인 원소)를 pop 한다. 3번 노드는 전 단계에서 방문했으므로 무시하고 건너뛴다.


[step ~] 위와 동일하게 이미 방문하여 처리가 끝난 노드이면 무시하고 건너뛰고, 방문한 적이 없다면 해당 노드를 처리한다.



개선된 다익스트라 알고리즘 코드 (Python)

단계마다 방문하지 않은 노드 중에서 최단 거리가 가장 짧은 노드를 선택하기 위해 최소힙(Heap) 기반 우선순위 큐 자료구조를 이용한다.

개선된 다익스트라 알고리즘이 동작하는 기본 원리는 동일하다.

  • 현재 가장 가까운 노드를 저장해 놓기 위해서 힙 자료구조를 추가적으로 이용한다는 점이 다르다.
  • 현재의 최단 거리가 가장 짧은 노드를 선택해야 하므로 최소 힙을 사용한다. (Python은 기본 우선순위 큐가 최소힙 기반이다.)
import sys
import heapq

input = sys.stdin.readline
INF = int(1e9)
n, m = map(int, input().split())
start = int(input())

graph = [[] for _ in range(n + 1)]
distance = [INF] * (n + 1)

for _ in range(m):
    a, b, c = map(int, input().split())
    graph[a].append((b, c))


def dijkstra(start):
    q = []
    heapq.heappush(q, (0, start))
    distance[start] = 0

    while q:
        dist, now = heapq.heappop(q)
        # 현재 노드가 이미 처리된 적이 있는 노드라면 무시
        # --> 꺼낸 노드 = 현재 최소 비용을 갖는 노드
        # 해당 노드의 비용이 현재 distance 배열에 기록된 비용보다 크다면 최단 경로가 아니기 때문에 무시해도 상관 X

        # 만약 이 조건을 생략하게 된다면 이미 방문한 정점을 중복하여 탐색하게 됨.
        # 그렇다면 큐에 있는 모든 다음 노드에 대한 인접노드에 대한 탐색을 다시 진행하게 된다.
        # 또한, 주어진 그래프가 완전 그래프이고 이 조건이 없다면 시간 복잡도는 O(E^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):
    if distance[i] == INF:
        print("INFINITY")
    else:
        print(distance[i])

# sample input
# 6 11
# 1
# 1 2 2
# 1 3 5
# 1 4 1
# 2 3 3
# 2 4 2
# 3 2 3
# 3 6 5
# 4 3 3
# 4 5 1
# 5 3 1
# 5 6 2

  • 시간 복잡도는 O(ElogV)
    노드를 하나씩 꺼내 검사하는 반복문(while문)은 노드의 개수 V 이상의 횟수로는 반복되지 않는다. 또한 V번 반복될 때마다 각각 자신과 연결된 간선들을 모두 확인한다. 따라서 '현재 우선순위 큐에서 꺼낸 노드와 연결된 다른 노드들을 확인'하는 총 횟수는 총 최대 간선의 개수(E)만큼 연산이 수행될 수 있다.
    _
    결과적으로 전체 다익스트라 최단 경로 알고리즘은 E개의 원소를 우선순위 큐에 넣었다가 모두 빼내는 연산과 매우 유사하다고 볼 수 있다. (이는 O(ElogE)의 시간복잡도를 가진다.) 중복 간선을 포함하지 않는 경우에 이를 O(ElogV)로 볼 수 있다. 왜냐하면 모든 노드끼리 서로 다 연결되어 있다고 했을 때 간선의 개수를 V^2로 볼 수 있으며 E는 항상 V^2 이하이기 때문이다.

사진 출처 : https://freedeveloper.tistory.com/277?category=888096

profile
Computer Science!!

0개의 댓글