단일 출발지 최단 경로 문제는 방향성 가중치 그래프에서 주어진 출발 정점에서 다른 모든 정점까지의 최단 경로를 찾는 문제다. 이 문제에는 여러 변형이 존재한다. 간선에 음수 값이 허용되는지, 사이클이 존재하는지, 특정 정점 쌍 사이의 경로를 찾는지 등에 따라 문제가 달라진다.
벨만-포드 최단 경로 알고리즘은 주어진 시작 정점 s
에서 음수 가중치를 포함할 수 있는 방향 그래프의 모든 정점까지의 최단 경로를 찾는다. 알고리즘은 그래프의 모든 간선에 대해 반복적으로 작업을 수행하며, 현재까지 알려진 최단 경로 가중치를 업데이트하는 과정을 거친다. 이때, 경로는 그래프에 존재하는 정점 수를 초과할 수 없으므로, 모든 간선에 대해 |V|-1
번 반복하면 충분히 모든 가능한 경로를 비교할 수 있다.
각 단계에서, 각 정점 v
에 대해 현재까지 알려진 최단 경로 s
~>v
의 가중치를 저장한다. 시작 정점 s
의 값은 0으로 유지하고, 나머지 정점의 값은 초기에 ∞
로 설정한다. 그런 다음, 각 간선 e = (u, v)
(여기서 u
는 방향 간선의 출발 정점, v
는 도착 정점)에 대해 다음 조건을 적용하여 값을 "완화"한다:
if weights[v] > weights[u] + e.weight {
weights[v] = weights[u] + e.weight
}
벨만-포드 알고리즘은 기본적으로 최단 경로의 길이만 계산하지만, 선택적으로 시작 정점에서 각 정점까지의 최단 경로에서 이전 정점을 기억하는 구조를 유지할 수 있다. 이 구조를 통해 도착 정점에서 시작 정점까지의 경로를 재구성할 수 있다. 이는 위의 if
문 내부에 다음 구문을 추가하여 구현한다:
predecessors[v] = u
다음의 가중치 방향 그래프를 살펴보자:
정점 s
에서 시작하는 최단 경로를 계산해 보자. 먼저 weights
와 predecessors
구조를 다음과 같이 준비한다:
weights | predecessors |
---|---|
weights[s] = 0 |
predecessors[s] = 1 |
weights[t] = ∞ |
predecessors[t] = ø |
weights[x] = ∞ |
predecessors[x] = ø |
weights[y] = ∞ |
predecessors[y] = ø |
weights[z] = ∞ |
predecessors[z] = ø |
각 이완(relaxation) 반복 후의 상태를 보자. 각 반복은 모든 간선을 한 번씩 처리하며, 이 그래프에서는 총 4번의 반복이 수행된다.
가중치 | 선행 노드 |
---|---|
weights[s] = 0 |
predecessors[s] = s |
weights[t] = 6 |
predecessors[t] = s |
weights[x] = 4 |
predecessors[x] = y |
weights[y] = 7 |
predecessors[y] = s |
weights[z] = 2 |
predecessors[z] = t |
가중치 | 이전 노드 |
---|---|
weights[s] = 0 |
predecessors[s] = s |
weights[t] = 2 |
predecessors[t] = x |
weights[x] = 4 |
predecessors[x] = y |
weights[y] = 7 |
predecessors[y] = s |
weights[z] = 2 |
predecessors[z] = t |
가중치 | 이전 노드 |
---|---|
weights[s] = 0 |
predecessors[s] = s |
weights[t] = 2 |
predecessors[t] = x |
weights[x] = 4 |
predecessors[x] = y |
weights[y] = 7 |
predecessors[y] = s |
weights[z] = -2 |
predecessors[z] = t |
가중치 | 이전 노드 |
---|---|
weights[s] = 0 |
predecessors[s] = s |
weights[t] = 2 |
predecessors[t] = x |
weights[x] = 4 |
predecessors[x] = y |
weights[y] = 7 |
predecessors[y] = s |
weights[z] = -2 |
predecessors[z] = t |
이 솔루션 구조의 또 다른 유용한 특성은 그래프에 음수 가중치 사이클이 존재하는지, 그리고 해당 사이클이 시작 정점에서 도달 가능한지 여부를 확인할 수 있다는 점이다. 음수 가중치 사이클은 간선 가중치의 합이 음수인 사이클을 의미한다. 이는 특정 시작 정점에서의 최단 경로가 잘 정의되지 않음을 나타내는데, 사이클을 반복해서 들어가면 경로의 가중치를 -∞
로 계속 줄일 수 있기 때문이다. 경로를 완전히 완화한 후, 각 간선 e = (u, v)
에 대해 v
까지의 최단 경로 가중치가 u
까지의 경로 가중치에 간선의 가중치를 더한 값보다 큰지 확인하면, 해당 간선이 음수 가중치를 가지고 있어 최단 경로의 가중치를 더 줄일 수 있음을 알 수 있다. 이미 충분한 횟수로 완화를 수행했으므로, 이 추가적인 가중치 감소가 무한히 계속될 것이라고 안전하게 가정할 수 있다.
이 예제에서는 a
에서 시작하는 최단 경로를 계산해 본다:
a
->t
->s
->a
사이클의 총 간선 가중치는 -9이다. 따라서 a
>>t
와 a
s
의 최단 경로는 정의되지 않는다. a
~>b
또한 b
->t
->s
가 음수 가중치 사이클이기 때문에 정의되지 않는다.
이것은 relaxation 루프를 실행하고, 위에서 언급한 모든 간선을 확인한 후에 확인된다. 이 그래프에서 relaxation을 수행한 후의 결과는 다음과 같다:
weights |
---|
weights[a] = -5 |
weights[b] = -5 |
weights[s] = -18 |
weights[t] = -3 |
이후 수행할 간선 확인 중 하나는 다음과 같다:
e = (s, a)
e.weight = 4
weight[a] > weight[s] + e.weight => -5 > -18 + 4 => -5 > -14 => true
이 확인 결과가 참이므로, 그래프에는 a
에서 도달 가능한 음수 가중치 사이클이 존재한다는 것을 알 수 있다.
이완 단계는 단순히 비교 작업을 수행하기 때문에 상수 시간(O(1)
)이 소요된다. 이 단계는 각 간선마다 한 번씩(Θ(|E|)
) 수행되며, 간선은 |V|-1
번 반복된다. 이는 전체 복잡도가 Θ(|V||E|)
임을 의미하지만, 최적화를 통해 개선할 수 있다. 외부 루프가 실행되고 기록된 가중치에 변경 사항이 없으면 이완 단계를 안전하게 종료할 수 있다. 이 경우 Θ(|V|)
단계 대신 O(|V|)
단계로 실행될 수 있다(즉, 어떤 크기의 그래프에서도 최선의 경우는 상수 번의 반복이며, 최악의 경우 여전히 |V|-1
번 반복된다).
음수 가중치 사이클을 확인하는 작업은 한 번 발견하면 즉시 반환하므로 O(|E|)
이다. 출발 정점에서 도달 가능한 모든 음수 가중치 사이클을 찾으려면 Θ(|E|)
번 반복해야 한다(현재는 사이클을 보고하지 않고, 단순히 사이클이 존재할 경우 nil
을 반환한다).
따라서 벨만-포드 알고리즘의 총 실행 시간은 O(|V||E|)
이다.
Swift Algorithm Club을 위해 Andrew McKnight가 작성함