벨만-포드
벨만-포드(Bellman-Ford) 알고리즘은 한 출발 정점에서 다른 모든 정점까지의 최단 거리를 구하는 알고리즘입니다. 다익스트라와 목적은 같지만 음수 가중치 간선이 있어도 동작하고, 음수 사이클이 있는지도 알려 줍니다. 방법은 단순합니다. 모든 간선을 한 번씩 훑으며 "이 간선을 거치면 더 짧아지는가"를 확인하는 일(완화, relaxation)을 V-1번 반복합니다.
다익스트라는 한 번 확정한 거리를 다시 고치지 않는다는 가정 위에 서 있어서 음수 간선을 만나면 틀릴 수 있습니다. 벨만-포드는 확정하지 않고 거리를 계속 줄여 가므로 그 가정이 필요 없습니다. 대신 모든 간선을 여러 번 훑어서 느립니다. 이 토픽에서는 라운드가 반복되며 거리가 줄어드는 모습, 간선 하나를 완화하는 순간, 음수 사이클에서 알고리즘이 어떻게 이상을 알리는지를 봅니다.
핵심 개념
완화(relaxation)
간선 u → v에 대해 d[u] + 가중치가 d[v]보다 작으면 d[v]를 그 값으로 줄입니다. 지금까지 알려진 경로보다 짧은 길을 발견했을 때 거리를 갱신하는 연산입니다.
핵심 개념
V-1번 반복
음수 사이클이 없으면 최단 경로는 정점을 중복해서 지나지 않으므로 간선을 많아야 V-1개 씁니다. 라운드를 i번 돌리면 간선 i개 이하로 가는 최단 경로가 반영되어 V-1번이면 충분합니다.
핵심 개념
음수 가중치
간선의 가중치가 음수여도 됩니다. 거리를 확정하지 않고 계속 줄일 수 있어서, 나중에 발견한 음수 간선이 앞서 구한 값을 더 낮추는 경우도 반영됩니다.
핵심 개념
음수 사이클
가중치 합이 음수인 사이클입니다. 한 바퀴 돌 때마다 거리가 줄어서 출발점에서 닿을 수 있다면 최단 거리가 정해지지 않습니다.
핵심 개념
V번째 확인
V-1번 반복한 뒤 한 번 더 모든 간선을 완화해 봅니다. 그래도 줄어드는 간선이 있다면 출발 정점에서 닿을 수 있는 음수 사이클이 있다는 신호입니다.
핵심 개념
조기 종료
한 라운드에서 갱신이 없으면 그 뒤로도 변하지 않으므로 반복을 멈출 수 있습니다. 간선을 훑는 순서에 따라 필요한 라운드 수는 달라집니다.
벨만-포드의 배경
이 알고리즘은 리처드 벨만(Richard Bellman)이 1958년 논문 "On a routing problem"에서 동적 계획법으로 최단 경로 문제를 푸는 방식으로 소개했고, 레스터 포드(Lester R. Ford Jr.)가 1956년 거리를 완화로 줄여 가는 일반적인 틀을 먼저 제시했습니다. 에드워드 무어(Edward F. Moore)도 1957년 학회에서 같은 방식을 발표해서(논문은 1959년) 벨만-포드-무어 알고리즘이라고 부르기도 합니다.
숫자로 확인하기
자주 하는 오해
면접에서 이렇게 물어요
면접 질문
벨만-포드가 다익스트라와 다른 점은?
음수 가중치 간선을 처리하고 음수 사이클을 탐지합니다. 다익스트라는 거리를 확정하며 진행해서 음수 간선이 있으면 틀릴 수 있습니다. 대신 벨만-포드는 O(VE)로 더 느립니다.
면접 질문
왜 V-1번만 반복하면 되나요?
음수 사이클이 없으면 최단 경로는 정점을 중복해서 지나지 않아 간선을 많아야 V-1개 씁니다. 라운드를 i번 돌리면 간선 i개 이하의 최단 경로가 반영되므로 V-1번이면 모든 최단 거리가 확정됩니다.
면접 질문
음수 사이클은 어떻게 찾나요?
V-1번 완화한 뒤 한 번 더 모든 간선을 완화해 봅니다. 거리가 갱신되는 간선이 있으면 출발 정점에서 닿을 수 있는 음수 사이클이 있다는 뜻입니다.
면접 질문
환율 차익거래(arbitrage)를 찾으려면?
환율에 로그를 취하고 부호를 바꾼 값을 간선 가중치로 두면 환율의 곱이 1보다 큰 순환이 가중치 합이 음수인 사이클이 됩니다. 벨만-포드의 음수 사이클 탐지로 찾습니다.
실제 시스템에서는
함께 보면 좋은 토픽
참고 자료
Python — 벨만-포드와 음수 사이클 탐지
INF = float('inf') def bellman_ford(n, edges, src): dist = [INF] * n dist[src] = 0 for _ in range(n - 1): # V-1번 반복 for u, v, w in edges: if dist[u] != INF and dist[u] + w < dist[v]: dist[v] = dist[u] + w # 완화(relax) for u, v, w in edges: # 한 번 더 확인 if dist[u] != INF and dist[u] + w < dist[v]: return None # 음수 사이클 return dist # S=0, A=1, B=2, C=3, D=4 · (출발, 도착, 가중치) E = [(3, 4, 2), (2, 3, 4), (1, 2, -3), (0, 1, 4), (0, 2, 5), (1, 3, 8), (2, 4, 7)] print(bellman_ford(5, E, 0)) # [0, 4, 1, 5, 7] print(bellman_ford(5, E + [(2, 1, 1)], 0)) # None (A → B → A 합 -2)
벨만-포드는 모든 간선을 한 번씩 완화하는 일을 V-1번 반복합니다. 완화는 출발 쪽 거리에 가중치를 더한 값이 도착 쪽의 현재 거리보다 작으면 도착 쪽 거리를 그 값으로 줄이는 연산입니다. 음수 가중치 간선 A → B(-3)이 있어도 정답 [0, 4, 1, 5, 7]이 나옵니다. 다익스트라는 거리를 한 번 확정하면 고치지 않기 때문에 이런 그래프에서 틀릴 수 있습니다. 음수 사이클이 없으면 최단 경로는 간선을 많아야 V-1개 지나므로 V-1번이면 거리가 확정됩니다. 그 뒤 한 번 더 확인했을 때도 줄어드는 간선이 있으면 출발 정점에서 닿을 수 있는 음수 사이클이 있다는 뜻이고, 함수는 None을 돌려줍니다. 간선 B → A(1)을 더하면 A → B → A의 합이 -3 + 1 = -2라 음수 사이클이 생겨 None이 나옵니다. 시각화의 라운드 반복 탭과 음수 사이클 탭이 보여 주는 과정과 같고, 간선 E개를 V번 훑으므로 O(VE)입니다.
Python — 일찍 끝내기와 간선 순서의 영향
def bellman_ford(n, edges, src): dist = [INF] * n dist[src] = 0 rounds = 0 for _ in range(n - 1): rounds += 1 changed = False for u, v, w in edges: if dist[u] != INF and dist[u] + w < dist[v]: dist[v] = dist[u] + w changed = True if not changed: # 한 라운드 동안 갱신이 없으면 끝 break for u, v, w in edges: if dist[u] != INF and dist[u] + w < dist[v]: return None, rounds return dist, rounds print(bellman_ford(5, E, 0)) # ([0, 4, 1, 5, 7], 4) good = [(0, 1, 4), (1, 2, -3), (0, 2, 5), (2, 3, 4), (1, 3, 8), (2, 4, 7), (3, 4, 2)] print(bellman_ford(5, good, 0)) # ([0, 4, 1, 5, 7], 2)
한 라운드 동안 갱신이 하나도 없으면 이후 라운드에서도 거리가 변하지 않으므로 V-1번을 채우지 않고 끝낼 수 있습니다. 이 조기 종료를 넣으면 간선을 확인하는 순서가 라운드 수에 영향을 줍니다. 같은 그래프에서 간선을 앞쪽 정점에서 뒤쪽 정점 쪽으로 가는 순서(S → A, A → B, …)로 놓으면 라운드 1에서 모든 거리가 확정되고 라운드 2에서 갱신이 없음을 확인하며 끝납니다. 시각화처럼 불리한 순서로 놓으면 4라운드가 모두 필요합니다. 최악의 경우는 순서와 상관없이 V-1번이라 시간 복잡도는 그대로 O(VE)입니다.
Java — 벨만-포드
record Edge(int from, int to, int w) {} static final int INF = Integer.MAX_VALUE; static int[] bellmanFord(int n, List<Edge> edges, int src) { int[] dist = new int[n]; Arrays.fill(dist, INF); dist[src] = 0; for (int i = 0; i < n - 1; i++) { for (Edge e : edges) { if (dist[e.from()] != INF && dist[e.from()] + e.w() < dist[e.to()]) { dist[e.to()] = dist[e.from()] + e.w(); } } } for (Edge e : edges) { // 음수 사이클 확인 if (dist[e.from()] != INF && dist[e.from()] + e.w() < dist[e.to()]) { return null; } } return dist; } // 같은 그래프: [0, 4, 1, 5, 7], B → A(1)을 더하면 null
자바 버전도 구조가 같습니다. INF를 Integer.MAX_VALUE로 두면 INF에 양의 가중치를 더할 때 넘쳐 음수가 되고, 음의 가중치를 더하면 INF보다 작아져 닿지 못한 정점이 갱신된 것처럼 됩니다. 그래서 출발 쪽 거리가 INF가 아닐 때만 더하도록 검사합니다. 위의 파이썬 예와 같은 그래프에서 결과는 [0, 4, 1, 5, 7]이고, B → A(1) 간선을 더하면 확인 단계에서 null을 돌려줍니다. 실무에서는 음수 가중치가 없다면 보통 다익스트라가 더 빠르지만, 환율이나 비용처럼 음수 간선이 생길 수 있는 문제나 음수 사이클 자체를 찾고 싶은 문제에는 벨만-포드를 씁니다.
Q1
벨만-포드 알고리즘이 다익스트라 알고리즘과 달리 처리할 수 있는 것은?
Q2
벨만-포드 알고리즘의 시간 복잡도는? (V: 정점 수, E: 간선 수)
Q3
벨만-포드로 음수 사이클을 탐지하는 방법은?