Computer >> 컴퓨터 >  >> 프로그래밍 >> 프로그래밍

벨만-포드 알고리즘: 음수 가중치까지 처리하는 단일 출발점 최단 경로


단일 출발점 최단 경로(Single Source Shortest Path) 알고리즘은 양수든 음수든 임의의 가중치를 가진 그래프에서 시작 정점으로부터 다른 모든 정점까지의 최소 거리를 구하는 알고리즘이며, 일반적으로 벨만-포드(Bellman-Ford) 알고리즘이라고 불립니다. 다익스트라(Dijkstra) 알고리즘과의 가장 큰 차이점은, 다익스트라 알고리즘에서는 음수 가중치를 처리할 수 없지만 벨만-포드 알고리즘에서는 이를 손쉽게 처리할 수 있다는 점입니다.

벨만-포드 알고리즘: 음수 가중치까지 처리하는 단일 출발점 최단 경로

동작 원리

벨만-포드 알고리즘은 상향식(bottom-up) 방식으로 거리를 계산합니다. 먼저 경로에 간선이 하나만 있는 경우의 거리부터 구하고, 이후 경로 길이를 한 단계씩 늘려가며 가능한 모든 해를 찾아냅니다. 핵심 연산은 간선 완화(relaxation)로, 어떤 간선 (u, v)에 대해 dist[u] + cost(u, v)가 현재 dist[v]보다 작으면 dist[v]를 갱신하는 과정을 말합니다. 이 과정을 정점 수에서 1을 뺀 횟수만큼 반복하면 최단 거리가 확정되며, 마지막에 간선 완화가 더 이상 일어나지 않는지 검사하여 음수 사이클(negative cycle)의 존재 여부까지 판별할 수 있습니다.

입력 및 출력 예시

입력 − 그래프의 비용 행렬:

0 6 ∞ 7 ∞
∞ 0 5 8 -4
∞ -2 0 ∞ ∞
∞ ∞ -3 0 9
2 ∞ 7 ∞ 0

출력 − 출발 정점(Source Vertex): 2

Vert: 0 1 2 3 4
Dist: -4 -2 0 3 -6
Pred: 4 2 -1 0 1
그래프에는 음수 간선 사이클이 없습니다

여기서 Vert는 정점 번호, Dist는 출발 정점으로부터의 최단 거리, Pred는 해당 정점 직전의 선행자(predecessor) 정점을 의미합니다.

알고리즘

bellmanFord(dist, pred, source)

입력 − 거리 리스트(dist), 선행자 리스트(pred), 출발 정점(source).

출력 − 음수 사이클이 발견되면 true, 아니면 false.

Begin
    iCount := 1
    maxEdge := n * (n - 1) / 2   // n은 정점의 개수
    for 그래프의 모든 정점 v에 대해, do
        dist[v] := ∞
        pred[v] := ϕ
    done
    dist[source] := 0
    eCount := 그래프에 존재하는 간선의 개수
    edgeList라는 이름의 간선 리스트 생성
    while iCount < n, do
        for i := 0 to eCount, do
            if dist[edgeList[i].v] > dist[edgeList[i].u] + (간선 i의 비용 cost[u,v]), then
                dist[edgeList[i].v] := dist[edgeList[i].u] + (간선 i의 비용 cost[u,v])
                pred[edgeList[i].v] := edgeList[i].u
            done
        done
        iCount := iCount + 1
    done
    for 그래프의 모든 간선 i에 대해, do
        if dist[edgeList[i].v] > dist[edgeList[i].u] + (간선 i의 비용 cost[u,v]), then
            return true   // 음수 사이클 존재
        done
    done
    return false          // 음수 사이클 없음
End

C++ 구현 예제

#include<iostream>
#include<iomanip>
#define V 5
#define INF 999
using namespace std;
// 그래프(방향 그래프)의 비용 행렬, 정점 5개
int costMat[V][V] = {
    {0, 6, INF, 7, INF},
    {INF, 0, 5, 8, -4},
    {INF, -2, 0, INF, INF},
    {INF, INF, -3, 0, 9},
    {2, INF, 7, INF, 0}
};
typedef struct{
    int u, v, cost;
}edge;
int isDiagraph(){
    // 그래프가 방향 그래프인지 확인
    int i, j;
    for(i = 0; i<V; i++){
        for(j = 0; j<V; j++){
            if(costMat[i][j] != costMat[j][i]){
                return 1; // 방향 그래프임
            }
        }
    }
    return 0; // 무방향 그래프임
}
int makeEdgeList(edge *eList){
    // 그래프의 간선들로부터 간선 리스트 생성
    int count = -1;
    if(isDiagraph()){
        for(int i = 0; i<V; i++){
            for(int j = 0; j<V; j++){
                if(costMat[i][j] != 0 && costMat[i][j] != INF){
                    count++; // 방향 그래프일 때 간선 발견
                    eList[count].u = i; eList[count].v = j;
                    eList[count].cost = costMat[i][j];
                }
            }
        }
    }else{
        for(int i = 0; i<V; i++){
            for(int j = 0; j<i; j++){
                if(costMat[i][j] != INF){
                    count++; // 무방향 그래프일 때 간선 발견
                    eList[count].u = i; eList[count].v = j;
                    eList[count].cost = costMat[i][j];
                }
            }
        }
    }
    return count+1;
}
int bellmanFord(int *dist, int *pred,int src){
    int icount = 1, ecount, max = V*(V-1)/2;
    edge edgeList[max];
    for(int i = 0; i<V; i++){
        dist[i] = INF;  // 무한대로 초기화
        pred[i] = -1;   // 아직 선행자 없음
    }
    dist[src] = 0;      // 시작 정점의 거리는 0
    ecount = makeEdgeList(edgeList); // 간선 리스트 생성
    while(icount < V){  // 반복 횟수는 (정점 수 - 1)
        for(int i = 0; i<ecount; i++){
            if(dist[edgeList[i].v] > dist[edgeList[i].u] + costMat[edgeList[i].u][edgeList[i].v]){
                // 간선 완화(relaxation) 후 선행자 설정
                dist[edgeList[i].v] = dist[edgeList[i].u] + costMat[edgeList[i].u][edgeList[i].v];
                pred[edgeList[i].v] = edgeList[i].u;
            }
        }
        icount++;
    }
    // 음수 사이클 검사
    for(int i = 0; i<ecount; i++){
        if(dist[edgeList[i].v] > dist[edgeList[i].u] + costMat[edgeList[i].u][edgeList[i].v]){
            return 1; // 그래프에 음수 사이클 존재
        }
    }
    return 0; // 음수 사이클 없음
}
void display(int *dist, int *pred){
    cout << "Vert: ";
    for(int i = 0; i<V; i++)
        cout <<setw(3) << i << " ";
    cout << endl;
    cout << "Dist: ";
    for(int i = 0; i<V; i++)
        cout << setw(3) << dist[i] << " ";
    cout << endl;
    cout << "Pred: ";
    for(int i = 0; i<V; i++)
        cout << setw(3) << pred[i] << " ";
    cout << endl;
}
int main(){
    int dist[V], pred[V], source, report;
    source = 2;
    report = bellmanFord(dist, pred, source);
    cout << "Source Vertex: " << source<<endl;
    display(dist, pred);
    if(report)
        cout << "The graph has a negative edge cycle" << endl;
    else
        cout << "The graph has no negative edge cycle" << endl;
}

실행 결과

Source Vertex: 2
Vert: 0 1 2 3 4
Dist: -4 -2 0 3 -6
Pred: 4 2 -1 0 1
The graph has no negative edge cycle

시간 복잡도 및 특징 정리

벨만-포드 알고리즘의 시간 복잡도는 O(V × E)입니다. 정점 수에서 1을 뺀 만큼 전체 간선을 순회하기 때문입니다. 다익스트라 알고리즘(O(E log V))보다는 느리지만, 음수 가중치 간선을 다룰 수 있고 음수 사이클의 존재 여부까지 검출할 수 있다는 강력한 장점이 있습니다. 따라서 음수 가중치가 포함된 그래프의 최단 경로 문제나 환율 차익거럼 탐지 등 음수 사이클 검출이 필요한 문제에서 널리 활용됩니다.