숨숨 베이스

지식이 숨어있는 공간

다익스트라

Last updated on December 12, 2025

1. 다익스트라란?

양수인 가중치만 있는 그래프 에서 시작 정점을 VstartV_{start} 라고 할 때, VstartV_{start}에서 다른 모든 정점까지의 최단 경로를 구하는 알고리즘을 뜻한다.

2. 원리

KEY WORD: GREEDY ALGORITHM
  1. 출발 정점으로부터 타 정점까지의 거리를 보여주는 dash board 만들기
  1. 아직 방문하지 않은 정점들 중, 출발 정점에서 최소 비용으로 갈 수 있는 정점 방문 (방문 처리 필요)
  1. 방문한 정점의 진출 간선을 활용하여 dash board 갱신
  1. 모든 정점까지의 최단 거리가 판가름날 때까지 반복
위와 같이 매 Loop마다 그때의 최소 비용 정점을 방문하기 때문에 Greedy Algorithm으로 볼 수 있다.

3. 예시

다음과 같은 그래프가 존재한다고 해보자. 출발 정점은 1번 정점이다.
전체_그래프

(0) 대시보드 만들기

img.png
출발 정점에서부터 다른 정점까지의 거리를 저장하는 대시보드를 만들었다. 아직 계산을 안 했음으로, 나머지 정점까지의 거리는 infinite이다. 다만 출발 정점 스스로까지의 거리는 없으므로 0으로 초기화 한다.

(1) 미방문 정점 중 최소 비용 경로 정점 방문

현재는 1번이 미 방문 정점 중 최소 비용 경로 정점이므로, 방문한다.

(2) 방문한 정점의 진출 간선으로 대시 보드 갱신

갱신1.png
당연히 무한대보다, 진출 간선 가중치가 작으므로 다음과 같이 대시보드가 갱신될 것이다.
갱신2.png

(3) 반복

모든 정점까지의 최소 비용 경로가 결정날 때까지 반복한다.
다음으로 최소비용으로 갈 수 있는 정점은 3번이다.
3번을 방문했다는 것은, 3번은 이미 출발 정점에서 그곳으로 갈 수 있는 최소 비용이 결정되었다는 뜻이다. (이유 후술)
갱신_그래프2.png
이후 다음과 같이 대시보드를 갱신한다.
대시보드2.png
이제 다음으로 미방문 정점 중 최소 비용인 2번 정점을 방문하여 처리한다.
갱신_그래프3.png
이떄 4번으로 가는 경로는 새로운 경로가 더 비용이 적으므로 갱신됨을 알 수 있다.
대시보드3.png
다음의 과정들은 윗 과정들의 반복임으로 빠르게 알아보고 넘어가겠다.
대시보드4.png
갱신그래프4.png
갱신_그래프5.png
대시보드5.png
갱신그래프6.png
대시보드6.png

4. 방문한 정점이 나중에 더 최소 비용으로 갱신되진 않을까?

모든 간선이 양수 가중치를 가지는 게 보장되면, 절대 그럴 일이 없다.
다음과 같은 그래프를 다익스트라로 구해보자.
그래프_예시_사진_1.png
만약 출발지에서 다음 행선지로 A를 골랐다고 가정해보자.
  • 그렇다면 다익스트라의 원리에 따라 가중치 비교에서 i > 2 라고 볼 수 있다.
  • iK 모두 양수이기 때문에, 이후에 만나는 간선의 가중치인 i+K가 2보다 작아지는 것은 불가능하다.
위와 같은 이유로 매 순간 최소 비용의 정점을 고르는 GREEDY ALGORITHM식 풀이가 가능해진다.
또한 반복 계산 과정 중에 특정한 정점을 방문했다면, 해당 정점까지의 경로는 추후에 갱신될 일이 없으므로 출발지로부터 해당 정점까지의 최소 비용이 정해졌다고도 볼 수 있다.

5. 코드

(0) 간선 (Edge) 클래스

class Edge implements Comparable<Edge> { int vex; int cost; public Edge ( int vex, int cost) { this.vex = vex; this.cost = cost; } @Override public int compareTo(Edge ob) { return this.cost - ob.cost; } }
  • vex는 목적지 정점을 나타낸다.
  • cost는 인접 리스트를 만들 때는 인접 정점 사이의 가중치를 나타내지만, 다익스트라에서는 출발정점에서부터 누적된 가중치 값을 나타낸다.
    (프림 알고리즘이랑은 다른점. 프림은 간선 가중치 자체만 저장하여 운용됨)
  • compareTo() 함수는 다익스트라를 풀 때, 출발 정점에서부터 가장 최소 비용으로 갈 수 있는 정점을 고르기 위해 필요하다. 왜냐하면 PriorityQueue를 써서 풀 것이기 때문이다.

(1) 입력 받기 및 대시보드 초기화

static int [] dis; static ArrayList<ArrayList<Edge>> lists = new ArrayList<ArrayList<Edge>>(); public static void main(String [] args) throws IOException { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); // 1. 대시보드는 정점을 모두 표현할 수 있게, 정점의 개수만큼 길이를 가지고, 처음에는 초기화가 되지 않았다는 의미로 2^31 - 1로 초기화 int n = Integer.parseInt(br.readLine()); dis = new int [n+1]; Arrays.fill(dis, Integer.MAX_VALUE); // 2. 인접 리스트 초기화하기. for(int i = 0; i <= n; i++) { lists.add(new ArrayList<Edge>()); } for(int i = 0; i < m; i++) { StringTokenizer st = new StringTokenizer (br.readLine()); int start = Integer.parseInt(st.nextToken()); int end = Integer.parseInt(st.nextToken()): int weight = Integer.parseInt(st.nextToken()); lists.get(start).add(new Edge(end, weight)); } }

(2) 다익스트라 하기

public void dijstra(int start) { // 1. 가장 최소 비용 경로 뽑는 우선순위 큐 PriorityQueue<Edge> pq = new PriorityQUeue<>(); // 2. 시작점에 대한 초기화 pq.offer(new Edge(v,0)); dis[v] = 0; // 3. 실제 동작 while(!pq.isEmpty()){ Edge now = pq.poll(); int vex = now.vex; int cost = now.cost; // (1) 이미 존재하는 최소 비용보다 크면 어짜피 방문할 필요가 없다. if(cost > dis[vex]) continue; // (2) 방문 지점의 진출 차선으로 대시보드 갱신 for(Edge ob : lists.get(vex)) { if(dis[ob.vex] <= cost + ob.cost) continue; dis[ob.vex] = cost + ob.cost; pq.offer(new Edge(ob.vex, dis[ob.vex])); } } }

A. 우선순위 큐를 쓰는 이유?

우선순위 큐는 '현 시점 가장 최소 비용 경로로 갈 수 있는 목적지를 추가 연산 없이 찾기 위해' 사용한다.
우선순위 큐를 쓰지 않았다면, 매 계산마다 대시보드를 돌면서 방문하지 않았으면서도, 다음 최소 비용으로 갈 수 있는 노드를 찾아야 한다. 만약 정점의 개수를 V 개라 한다면, 매 방문 정점마다 대시보드 순회를 V번 반복한다. 즉 최종 시간복잡도가 O(V2)O(V^{2}) 가 되는 것이다.
하지만 우선순위 큐를 쓰면 해당 최소 비용 미방문 정점을 찾는 시간복잡도가 O(VlogV)O(V \log{V})로 내려간다.
  • 매 순회마다 우선순위큐에서 값을 빼면 되는데, 정점만큼 순회하니까 VV
  • 우선순위 큐의 경우, 정점으로 이루어진 heap 트리이다. 이때 루트값을 poll()빼고, 다시 max heap이든, min heap이든 복구하는 작업이 필요하다. 이때 최대 트리의 높이만큼 연산이 필요하므로 logV\log{V}의 연산이 필요하다.

B. 3번 동작의 (1)이 필요한 이유

이제 다음 방문할 정점을 우리가 직접 찾지 않고 우선순위 큐에 맡기기 때문에, 오래 전에 넣은 값이 시간 상 그보다 나중에 들어온 값보다 우선순위가 밀릴 수 있다. 따라서 방문도 했고, 갱신도 더 작은 값으로 한 상황에서 '옛날 버전의 간선' 을 만난 경우, 다음 연산을 진행해도 효용이 없으므로, 계산을 하지 않고 넘어간다.
 
ℹ️ 정점 방문처리는 따로 구현 안해도 된다.

위에서도 살펴 보았듯이 양수 가중치만 존재하는 그래프에서 하는 다익스트라 특성 상 이미 방문한 정점은 두 번 다시 큐 안에 들어오지 않기 떄문이다.
 

6. 시간 복잡도

우선순위 큐(힙) + 인접 리스트를 썼을 경우
정점의 개수를 V, 간선의 개수를 E라고 하자.
  1. 매 반복마다 우선순위큐에서 '최소 경로로 갈 수 있는 미방문 정점' 을 뽑아낸다.
    해당 과정은 정점의 개수만큼 반복되는 루프 속에서 정점으로만 이루어진 Heap의 루트값을 반환하는 작업이다. 루트값을 뺀 후에 heap을 다시 최소 혹은 최대 힙으로 복구하는 과정이 필요하므로 최종적으로
    O(VlogV)O(V \log{V}) 의 시간 복잡도가 든다.
  1. 현 방문 정점에서 갈 수 있는 간선들을 모두 확인 후, 최단 경로 비용이 줄었으면 다시 큐에 삽입한다. 최단 경로 비용이 기존보다 줄어드는 현상을 완화라고 했을 때, 완화현상은 매 간선 확인마다 이루어질 수 있다.
    따라서 큐에 삽입하는 값은 간선의 개수만큼 이루어지며, 그때마다 힙을 정돈하는 연산은 logV만큼 필요하다 따라서 최종적으로 O(ElogV)O(E \log{V}) 만큼의 시간복잡도가 든다.
위의 두 과정은 연속되어 일어나지 않는다. 따라서 최종 시간복잡도는 O((V+E)logV)O((V+E) \log{V}) 이다.
 

Metadata

A. 모르는 것 정리

  • 최단 경로
    : A → B 까지의 최단 경로는 A에서 B까지 최소 비용으로 가는 경로를 말한다.
    (최소 비용은 그래프의 성질에 따라 다른데, 가중치 그래프에서는 가중치 합계 비용, 가중치 없는 그래프에서는 간선의 개수이다.)

⬅️ 이전 글
 
➡️ 다음 글