알고리즘

최단 경로 알고리즘 : 다익스트라, 벨만-포드, 플로이드-워셜

망또또의 언냐 2024. 9. 22. 20:20

최단 경로 문제들을 풀며, 다양한 알고리즘이 있다는 걸 알았다.

 

다 비슷한 모양의 코드같지만, 개념들을 확고히 다지고 알고 넘어가면 놓을 것 같아 정리해본다.

 


 

다익스트라, 벨만-포드, 플로이드-워셜 알고리즘은 그래프에서 최단 경로를 구하는 알고리즘이다.

세 가지 모두 그래프 문제를 해결할 때 유용하지만, 각각의 특성과 사용 조건이 다르다.

 

1. 다익스트라 알고리즘 (Dijkstra’s Algorithm)

개념:

다익스트라 알고리즘은 하나의 시작점에서 모든 다른 노드까지의 최단 경로를 구하는 알고리즘이다. 가중치가 음수가 없는 그래프에서만 동작하며, 최단 경로를 탐색할 때 우선순위 큐(최소 힙)를 사용해 가장 짧은 경로를 점차적으로 확장한다.

어떻게 동작하나?

  1. 시작 노드에서 출발해, 그 노드까지의 거리를 0으로 초기화한다. 나머지 노드는 무한대로 설정한다.
  2. 우선순위 큐에서 가장 작은 가중치를 가진 노드를 꺼내고, 이 노드와 연결된 이웃 노드들의 거리를 업데이트한다.
  3. 업데이트된 이웃 노드를 다시 큐에 넣고, 반복적으로 가장 짧은 경로를 찾는다.
  4. 큐가 비게 될 때까지 이 과정을 반복하면 모든 노드까지의 최단 경로를 알 수 있다.

예시:

예를 들어, 도시 A에서 B, C, D로 가는 그래프가 있다고 하자. 간선의 가중치는 각각 다음과 같다:

  • A → B (4), A → C (2), C → B (1), B → D (5), C → D (8)

이 그래프에서 다익스트라 알고리즘을 실행하면, A에서 D까지의 최단 경로는 A → C → B → D이고, 그 총 비용은 7이다.

언제 사용하나?

다익스트라는 주로 가중치가 양수인 경우에 최단 경로를 찾을 때 사용한다. 특히, 빠른 속도가 요구되는 대규모 네트워크에서 매우 유용하다.

장점과 단점:

  • 장점: 음수가 없는 그래프에서 매우 빠르다.
  • 단점: 음수 가중치가 포함된 그래프에서는 올바른 결과를 보장하지 못한다.

코드 구현

class MinHeap {
  constructor() {
    this.heap = [];
  }

  push([node, dist]) {
    this.heap.push([node, dist]);
    this.bubbleUp();
  }

  bubbleUp() {
    let idx = this.heap.length - 1;
    const last = this.heap[idx];
    while (idx > 0) {
      let parentIdx = Math.floor((idx - 1) / 2);
      if (this.heap[parentIdx][1] <= last[1]) break;
      this.heap[idx] = this.heap[parentIdx];
      idx = parentIdx;
    }
    this.heap[idx] = last;
  }

  pop() {
    const top = this.heap[0];
    const last = this.heap.pop();
    if (this.heap.length > 0) {
      this.heap[0] = last;
      this.bubbleDown();
    }
    return top;
  }

  bubbleDown() {
    let idx = 0;
    const length = this.heap.length;
    const node = this.heap[0];
    while (true) {
      let leftChildIdx = 2 * idx + 1;
      let rightChildIdx = 2 * idx + 2;
      let swapIdx = null;

      if (leftChildIdx < length && this.heap[leftChildIdx][1] < node[1]) {
        swapIdx = leftChildIdx;
      }
      if (rightChildIdx < length && this.heap[rightChildIdx][1] < (swapIdx === null ? node[1] : this.heap[swapIdx][1])) {
        swapIdx = rightChildIdx;
      }
      if (swapIdx === null) break;
      this.heap[idx] = this.heap[swapIdx];
      idx = swapIdx;
    }
    this.heap[idx] = node;
  }

  isEmpty() {
    return this.heap.length === 0;
  }
}

function dijkstra(start, graph, n) {
  const distances = Array(n).fill(Infinity);
  distances[start] = 0;
  const minHeap = new MinHeap();
  minHeap.push([start, 0]);

  while (!minHeap.isEmpty()) {
    const [currentNode, currentDist] = minHeap.pop();

    if (distances[currentNode] < currentDist) continue;

    for (const [nextNode, weight] of graph[currentNode]) {
      const newDist = currentDist + weight;
      if (newDist < distances[nextNode]) {
        distances[nextNode] = newDist;
        minHeap.push([nextNode, newDist]);
      }
    }
  }

  return distances;
}

 

조건:

  • 그래프에 음수 간선이 없을 때.
  • 단일 시작점에서 모든 노드까지 최단 경로를 구할 때

 


 

 

2. 벨만-포드 알고리즘 (Bellman-Ford Algorithm)

개념:

벨만-포드 알고리즘은 하나의 시작점에서 모든 노드까지의 최단 경로를 구하는 알고리즘으로, 음의 가중치가 있는 그래프에서도 동작한다. 이 알고리즘은 다익스트라와 달리 모든 간선을 반복적으로 확인해 최단 경로를 찾는다.

어떻게 동작하나?

  1. 모든 노드의 거리를 무한대로 초기화하고, 시작 노드의 거리는 0으로 설정한다.
  2. 모든 간선을 반복적으로 확인하며, 노드 간 거리를 업데이트한다. 각 노드에서 모든 간선을 최대 V-1번 반복 확인한다.
  3. 모든 간선을 다 돌고 나서도 경로가 업데이트된다면, 이는 음의 사이클이 존재함을 의미한다.

예시:

  • 노드 A → B (4), A → C (3), C → B (-2), B → D (5)

이 그래프에서 벨만-포드를 실행하면, A에서 D까지의 최단 경로는 A → C → B → D이고, 그 총 비용은 6이다. C → B의 음의 가중치를 고려해 최적 경로를 찾을 수 있다.

언제 사용하나?

벨만-포드는 음의 가중치가 있는 그래프에서 사용한다. 특히, 음의 사이클이 존재하는지 확인해야 할 때 매우 유용하다.

장점과 단점:

  • 장점: 음의 가중치가 있는 그래프에서도 최단 경로를 찾을 수 있다.
  • 단점: 모든 간선을 V-1번씩 확인하므로, 시간 복잡도가 **O(V * E)**로 느리다.
function bellmanFord(start, edges, n) {
  const distances = Array(n).fill(Infinity);
  distances[start] = 0;

  // V-1번 반복
  for (let i = 0; i < n - 1; i++) {
    for (const [u, v, weight] of edges) {
      if (distances[u] !== Infinity && distances[u] + weight < distances[v]) {
        distances[v] = distances[u] + weight;
      }
    }
  }

  // 음수 사이클 탐지
  for (const [u, v, weight] of edges) {
    if (distances[u] !== Infinity && distances[u] + weight < distances[v]) {
      return 'Negative cycle detected';
    }
  }

  return distances;
}

조건:

  • 음의 가중치가 있는 간선이 포함된 그래프에서 최단 경로를 구할 수 있다.
  • 음의 사이클이 있는지 확인할 때 유용하다.

3. 플로이드-워셜 알고리즘 (Floyd-Warshall Algorithm)

개념:

플로이드-워셜 알고리즘은 모든 노드 쌍 간의 최단 경로를 구하는 알고리즘이다. 벨만-포드나 다익스트라와는 다르게, 모든 노드에서 모든 노드까지의 경로를 한꺼번에 계산할 수 있다. 이 알고리즘은 동적 계획법을 기반으로 동작하며, 그래프 내의 모든 경로를 고려해 최단 경로를 찾는다.

어떻게 동작하나?

  1. 모든 노드 쌍의 거리를 행렬로 나타낸다. 만약 직접 연결된 간선이 없다면 무한대로 표시한다.
  2. 각 노드를 중간 노드로 설정하고, 이 중간 노드를 거쳐가는 경로가 더 짧은지 확인해 행렬을 업데이트한다.
  3. 이 과정을 노드의 수만큼 반복해, 모든 노드 쌍 간의 최단 경로를 계산한다.

예시:

노드 A, B, C가 있는 그래프에서, 각 경로가 다음과 같다고 하자:

  • A → B (5), A → C (9), B → C (2)

플로이드-워셜을 실행하면 A → C의 최단 경로는 A → B → C로, 그 비용은 7이 된다.

언제 사용하나?

플로이드-워셜은 모든 노드 쌍 간의 경로를 한꺼번에 계산해야 하는 경우에 사용한다. 노드 수가 적은 경우에는 매우 효과적이다. 예를 들어, 교통 네트워크 분석이나 모든 사람 간의 최단 경로를 계산해야 하는 사회적 연결망 분석에 유용하다.

장점과 단점:

  • 장점: 모든 노드 간 최단 경로를 동시에 계산할 수 있다.
  • 단점: 시간 복잡도가 **O(V³)**로, 노드 수가 많으면 비효율적이다.

 

function floydWarshall(graph, n) {
  const dist = Array.from({ length: n }, () => Array(n).fill(Infinity));

  for (let i = 0; i < n; i++) {
    dist[i][i] = 0;
  }

  for (let [u, v, weight] of graph) {
    dist[u][v] = weight;
  }

  for (let k = 0; k < n; k++) {
    for (let i = 0; i < n; i++) {
      for (let j = 0; j < n; j++) {
        if (dist[i][k] + dist[k][j] < dist[i][j]) {
          dist[i][j] = dist[i][k] + dist[k][j];
        }
      }
    }
  }

  return dist;
}

 

조건:

  • 모든 노드 쌍 간의 최단 경로를 구할 때 유용하다.
  • 노드 수가 적은 경우에 효율적이며, 시간 복잡도는 O(V³).
  • 음의 가중치가 포함된 그래프에서도 동작하지만, 음의 사이클은 탐지하지 않는다.
  •  

 

 

3. 플로이드 워셜 vs 다익스트라: 언제 어떤 알고리즘을 사용할까?

플로이드 워셜을 사용하는 경우:

  • 모든 정점 쌍 사이의 최단 경로를 구해야 한다.
    • 문제에서 "모든 정점 쌍 간의 최단 경로"가 필요하다고 명시되어 있는 경우.
    • 예시: 여행 경로 최적화, 모든 도시 쌍 간의 최단 경로, 모든 사람 간의 연결성 계산.
  • 정점의 수가 작다(대략 100 이하).
    • 정점 수가 많으면 O(N3)O(N^3)의 시간 복잡도가 성능에 큰 영향을 미치기 때문에 적합하지 않습니다.
  • 간접 경로를 반드시 고려해야 할 때.
    • 경유지나 중간 지점을 통해서 가는 경로를 탐색하는 경우.

다익스트라를 사용하는 경우:

  • 단일 출발점에서 모든 다른 정점까지의 최단 경로가 필요하다.
    • 예시: 한 출발지에서 목적지까지의 최단 경로 찾기.
  • 그래프가 크고(정점이나 간선이 많을 때), 양의 가중치만 있을 때.
    • 음의 가중치가 없고, 그래프가 큰 경우, 다익스트라의 시간 복잡도 O((N+E)log⁡N)O((N + E) \log N)가 성능 면에서 우수하다.
  • 출발점이 고정된 상황에서 최단 경로를 구해야 할 때.
    • 예를 들어, 도시 내의 특정 위치에서 모든 다른 위치로 가는 경로를 계산해야 할 때.

 


 

정리

  • 다익스트라: 음의 가중치가 없는 그래프에서 단일 시작점의 최단 경로를 구할 때 사용한다. 시간 복잡도가 효율적이며, 네트워크 문제나 경로 탐색에 적합하다.
  • 벨만-포드: 음의 가중치가 있는 그래프에서도 최단 경로를 구할 수 있다. 특히 음의 사이클을 탐지하는 문제에서 유용하다.
  • 플로이드-워셜: 모든 노드 쌍 간의 최단 경로를 구하는 문제에 적합하다. 노드 수가 적은 경우에는 효과적이지만, 대규모 그래프에서는 비효율적일 수 있다.

'알고리즘' 카테고리의 다른 글

Segment Tree - 구간 트리 2  (0) 2025.05.22
Segment Tree - 구간트리  (0) 2025.05.13
nodejs - 연결리스트  (0) 2024.06.20
시간복잡도 이해하기  (0) 2024.06.15
Nodejs - 이진탐색  (0) 2024.04.22