네비게이션은 어떻게 최단 경로를 찾을까요? 그 근본에는 Dijkstra 알고리즘이 있습니다.

최단 경로 문제 분류

문제 유형알고리즘음수 간선시간 복잡도
단일 출발점 (양수)DijkstraXO((V+E) log V)
단일 출발점 (음수 허용)Bellman-FordOO(VE)
모든 쌍Floyd-WarshallOO(V³)

Dijkstra 알고리즘

핵심 아이디어

시작 노드에서 가장 가까운 확정되지 않은 노드를 선택하고, 그 노드를 통해 이웃 노드의 거리를 갱신합니다. 그리디 알고리즘 입니다.

동작 과정

PLAINTEXT
시작: S, 목표: 모든 노드까지의 최단 거리

1. dist[S] = 0, 나머지 = ∞
2. 가장 가까운 미확정 노드 u를 선택 (그리디)
3. u의 이웃 v에 대해: dist[v] = min(dist[v], dist[u] + w(u,v))
4. u를 확정 (다시 방문하지 않음)
5. 모든 노드가 확정될 때까지 반복

우선순위 큐 구현

JAVA
public static int[] dijkstra(List<List<int[]>> graph, int start) {
    int n = graph.size();
    int[] dist = new int[n];
    Arrays.fill(dist, Integer.MAX_VALUE);
    dist[start] = 0;

    // (거리, 노드) min-heap
    PriorityQueue<int[]> pq = new PriorityQueue<>(
        Comparator.comparingInt(a -> a[0])
    );
    pq.offer(new int[]{0, start});

    while (!pq.isEmpty()) {
        int[] curr = pq.poll();
        int d = curr[0], u = curr[1];

        // 이미 더 짧은 경로로 확정되었으면 건너뜀
        if (d > dist[u]) continue;

        for (int[] edge : graph.get(u)) {
            int v = edge[0], w = edge[1];
            int newDist = dist[u] + w;

            if (newDist < dist[v]) {
                dist[v] = newDist;
                pq.offer(new int[]{newDist, v});
            }
        }
    }
    return dist;
}

경로 추적

최단 거리뿐만 아니라 경로도 필요하면 prev 배열을 유지합니다.

JAVA
int[] prev = new int[n];
Arrays.fill(prev, -1);

// 갱신 시
if (newDist < dist[v]) {
    dist[v] = newDist;
    prev[v] = u; // v의 이전 노드는 u
    pq.offer(new int[]{newDist, v});
}

// 경로 복원
public static List<Integer> getPath(int[] prev, int target) {
    List<Integer> path = new ArrayList<>();
    for (int node = target; node != -1; node = prev[node]) {
        path.add(node);
    }
    Collections.reverse(path);
    return path;
}

Dijkstra가 음수 간선에서 실패하는 이유

PLAINTEXT
    A --1--> B
    |         |
    5        -4
    |         |
    v         v
    C --2--> D

dist[B] = 1 (확정) → 하지만 A→C→D→B = 5+2+(-4) = 3... 아니, 이건 -4가 있어도 1이 더 짧네요.
실제 문제가 되는 경우:

    A --1--> B --(-10)--> C
    |                      ^
    +--------5-------------+

A→B→C = 1+(-10) = -9
A→C = 5
Dijkstra: A→C를 5로 확정한 후 B를 처리 → A→B→C = -9인데 이미 확정됨!

Bellman-Ford 알고리즘

모든 간선을 V-1번 반복하여 최단 거리를 갱신합니다. 음수 간선을 처리할 수 있고, 음수 사이클도 탐지합니다.

핵심 아이디어

최단 경로는 최대 V-1개의 간선을 포함합니다. 따라서 모든 간선을 V-1번 완화(relax)하면 모든 최단 경로가 구해집니다.

JAVA
public static int[] bellmanFord(int n, int[][] edges, int start) {
    int[] dist = new int[n];
    Arrays.fill(dist, Integer.MAX_VALUE);
    dist[start] = 0;

    // V-1번 반복
    for (int i = 0; i < n - 1; i++) {
        boolean updated = false;

        for (int[] edge : edges) {
            int u = edge[0], v = edge[1], w = edge[2];

            if (dist[u] != Integer.MAX_VALUE && dist[u] + w < dist[v]) {
                dist[v] = dist[u] + w;
                updated = true;
            }
        }

        if (!updated) break; // 조기 종료 (더 이상 갱신 없으면)
    }

    // 음수 사이클 탐지: V번째 반복에서도 갱신이 일어나면 음수 사이클 존재
    for (int[] edge : edges) {
        int u = edge[0], v = edge[1], w = edge[2];
        if (dist[u] != Integer.MAX_VALUE && dist[u] + w < dist[v]) {
            throw new IllegalStateException("음수 사이클이 존재합니다!");
        }
    }

    return dist;
}

시간 복잡도

  • O(VE): V-1번 반복 × E개 간선
  • Dijkstra보다 느리지만, 음수 간선을 처리할 수 있습니다

SPFA (Shortest Path Faster Algorithm)

Bellman-Ford를 큐로 최적화한 버전입니다. 갱신된 노드만 큐에 넣어 불필요한 반복을 줄입니다.

JAVA
public static int[] spfa(List<List<int[]>> graph, int start) {
    int n = graph.size();
    int[] dist = new int[n];
    Arrays.fill(dist, Integer.MAX_VALUE);
    dist[start] = 0;

    boolean[] inQueue = new boolean[n];
    Queue<Integer> queue = new LinkedList<>();
    queue.offer(start);
    inQueue[start] = true;

    while (!queue.isEmpty()) {
        int u = queue.poll();
        inQueue[u] = false;

        for (int[] edge : graph.get(u)) {
            int v = edge[0], w = edge[1];

            if (dist[u] + w < dist[v]) {
                dist[v] = dist[u] + w;

                if (!inQueue[v]) {
                    queue.offer(v);
                    inQueue[v] = true;
                }
            }
        }
    }
    return dist;
}

평균적으로 Bellman-Ford보다 빠르지만, 최악의 경우 여전히 O(VE)입니다.

A* 알고리즘 (Dijkstra의 확장)

A*는 Dijkstra에 휴리스틱(추정 거리) 을 더한 알고리즘입니다.

JAVA
// 우선순위 = 현재까지의 실제 거리 + 목표까지의 추정 거리
// f(n) = g(n) + h(n)
// g(n): 시작→n 실제 거리
// h(n): n→목표 추정 거리 (휴리스틱)

네비게이션에서는 직선 거리를 휴리스틱으로 사용합니다. 목표 방향으로의 탐색을 우선시하여 불필요한 탐색을 줄입니다.

음수 사이클의 의미

PLAINTEXT
A --(-1)--> B --(-1)--> C --(-1)--> A

이 사이클을 돌 때마다 거리가 -3씩 줄어듦
→ 무한히 돌면 거리가 -∞
→ "최단 경로" 자체가 정의되지 않음

백엔드/실무에서의 연결

OSPF 라우팅

OSPF(Open Shortest Path First)는 인터넷 라우팅 프로토콜입니다. 각 라우터가 네트워크 토폴로지를 파악하고 Dijkstra를 실행 하여 최단 경로 트리를 구축합니다.

PLAINTEXT
라우터 A → (cost 10) → 라우터 B → (cost 5) → 라우터 C
라우터 A → (cost 20) → 라우터 C

Dijkstra: A→B→C (cost 15) < A→C (cost 20)
→ C로의 패킷은 B를 경유

RIP 라우팅

RIP(Routing Information Protocol)은 Bellman-Ford 기반입니다. 각 라우터가 이웃에게 자신의 거리 벡터를 전파합니다.

네비게이션

Google Maps, Kakao 네비 등은 Dijkstra/A*의 변형을 사용합니다. 실제로는 도로 네트워크의 특성을 활용한 Contraction Hierarchies 같은 전처리 기법도 함께 사용합니다.

서비스 메쉬의 라우팅

MSA에서 서비스 간 호출 경로를 결정할 때, 지연 시간(latency)을 가중치로 한 최단 경로가 사용됩니다.

알고리즘 선택 가이드

PLAINTEXT
가중치가 없는가? → BFS
가중치가 0 또는 1? → 0-1 BFS
양수 가중치만? → Dijkstra
음수 가중치 있음? → Bellman-Ford / SPFA
모든 쌍 최단 경로? → Floyd-Warshall
단일 목표 + 휴리스틱? → A*

주의할 점

음수 간선이 있는 그래프에서 Dijkstra를 사용하는 실수

Dijkstra는 음수 간선에서 최적해를 보장하지 않습니다. "이미 확정된 최단 거리"가 나중에 음수 간선으로 더 짧아질 수 있기 때문입니다. 음수 간선이 있으면 Bellman-Ford를 사용해야 합니다.

Bellman-Ford에서 음수 사이클 탐지를 빠뜨리는 실수

V-1번 반복 후에도 갱신이 발생하면 음수 사이클이 존재합니다. 이 체크를 빠뜨리면 무한 루프에 빠지거나 잘못된 결과를 반환합니다.


정리

알고리즘시간 복잡도음수 간선음수 사이클 탐지사용처
DijkstraO((V+E) log V)XXOSPF, 네비게이션
Bellman-FordO(VE)OORIP, 음수 간선 그래프
SPFAO(VE) 최악OOBellman-Ford 최적화
A*O(b^d)XX게임, 네비게이션

기억할 포인트:

  • Dijkstra는 양수 가중치 그래프에서 가장 효율적인 단일 출발점 최단 경로 알고리즘입니다.
  • Bellman-Ford는 ** 음수 간선 **을 처리할 수 있고, V번째 반복에서 갱신이 일어나면 ** 음수 사이클 **입니다.
  • OSPF = Dijkstra, RIP = Bellman-Ford. 네트워크 라우팅 프로토콜이 이 알고리즘들의 실제 응용입니다.
  • Dijkstra에서 d > dist[u] 체크를 빼먹으면 성능이 크게 저하됩니다.
댓글 로딩 중...