PS/Algorithm

그래프 알고리즘 - (3) Dijkstra, Bellman-Ford, Floyd-Warshall

hyelie 2022. 6. 22. 12:52
이 글은 포스텍 오은진 교수님의 알고리즘(CSED331) 강의를 기반으로 재구성한 것입니다.

 

세 알고리즘 모두 '최단 거리'를 알아내는 알고리즘이다. 다만 어디서부터 어디까지 알아내는가가 다를 뿐이다.

 

1. dijkstra : O((|V| + |E|) * log|V|)

 한 vertex에서 다른 모든 vertex까지의 최단거리를 알아내는 알고리즘이다. BFS와 유사한 알고리즘으로, BFS는 queue를 사용하지만 dijkstra는 priority queue(heap)을 사용한다. 다만 dijkstra는 weight가 음수인 경우에는 풀리지 않는다!

procedure dijkstra(G, s)
    for all vertex u in V do
        dist(u) = INF
        prev(u) = null
    end for
    
    dist(s) = 0
    H = makeHeap(V)
    while H is not empty do
        u = deleteMin(H)
        for all edges (u, v) in E do
            if dist(v) > dist(u) + weight(u, v) then
                dist(v) = dist(u) + weight(u, v)
                prev(v) = u
                decreaeKey(H, v)
            end if
        end for
    end while
    
// makeHeap(V) : 모든 vertex로 priority queue를 생성
// deleteMin(H) : priority queue의 제일 작은 key를 가지는 element를 pop
// decreaseKey(H, v) : priority queue의 v에 해당하는 key를 갱신
  • 핵심은 edge (u, v)에 대해 dist(v) > dist(u) + weight(u, v)라면 dist(v)를 dist(u) + weight(u, v)로 설정하는 것이다.​

 

Time Complexity of dijkstra

 위 pseudo code에도 작성되어 있지만 제일 처음 dist(u)를 설정하는 과정에서 O(n), queue의 deletemin은 n time, decreasekey가 |E| time. 보통 priority queue는 binary tree를 이용하기 때문에 deletemin, insert, decreasekey은 O(log|V|)이기 때문에 O((|V| + |E|)log|V|)라고 기억하면 된다.

 

 

코드 : O((|V| + |E|) * log|V|)

struct mycomp{
    bool operator()(pii&a, pii&b){
        return a.second > b.second; // weight가 큰 것을 앞으로
    }
};

// edges[i] : i가 edge의 시작인 edge list
// edges[i][0].first : edge list의 edge end
// edges[i][0].second : edge list의 edge weight
vector<int> dijkstra(int start, vector<vector<pii>> edges){
    // dist initialize
    vector<int> dist(V+1, INF);
    dist[start] = 0;
    
    // heap initialize
    priority_queue<pii, vector<pii>, mycomp> pq;
    pq.push({start, 0});
    
    while(!pq.empty()){
        int cur = pq.top().first;
        int cur_weight = pq.top().second;
        pq.pop();

        // 같은 경로지만 cost가 다른 경우를 대비해 아래 로직 넣음
        if(dist[cur] < cur_weight) continue;
        
        // pq.top()에 연결된 모든 edge에 대해 dijkstra 수행
        for(pii &adjs : edges[cur]){
            int adj = adjs.first, adj_weight = adjs.second;
            
            // 만약 갱신 가능하다면 dist 갱신 이후 pq에 다시 넣음
            if(dist[adj] > dist[cur] + adj_weight){
                dist[adj] = dist[cur] + adj_weight;
                pq.push({adj, dist[adj]});
            }
        }
    }
    return dist;
}

 

 

 

2. Bellman-Ford : O(|V| * |E|)

 dijkstra는 negative weight edge가 존재하면 풀리지 않는다. 이런 경우를 해결하기 위해 bellman-ford 알고리즘이 나왔으며, 음수 사이클이 존재하는 경우에는 풀 수 없다.(계속 값이 갱신되기 때문) dijkstra의 더 큰 버전이니만큼 한 vertex부터 다른 모든 vertex까지의 최단거리를 구한다.

 음수 사이클이 존재하는지의 여부는 V번째 for문에서 값이 갱신되는지 여부로 알 수 있다. bellman-ford 알고리즘의 특성상 V-1번째 for문에서 모든 vertex까지의 최단거리가 나오는데, V번째 cycle에서 또 갱신된다면 음수 cycle이 있기 때문이다.


bellman-ford 알고리즘이 V-1번째 for문에서 모든 vertex까지의 최단거리가 결정되나?

proof) 
 vertex가 V개일 때, path π의 최장 길이는 V일 것이다. 한편 시작 vertex가 s일 때 dist[s]는 0인 것을 이미 알고 있다. 그러면 path π에서 s 다음에 있는 것을 w, w', w'', ...라고 두었을 때 dist[w]는 1번의 순회로 알 수 있고 dist[w']는 2번의 순회로 최단거리를 알 수 있다. 귀납적으로, path π의 제일 마지막 vertex x는 V-1번의 순회로 최단거리를 알 수 있다.

 만약 이 과정에서 같은 vertex v를 2번 이상 반복한다면 그 cycle은 negative weight를 가지게 된다. 그렇지 않다면 weight가 더 증가하기 때문에 v를 2번 이상 반복할 이유가 없기 때문이다. 다르게 표현하면 v를 2번 이상 반복하는 것이 없는 경우 V-1번의 순회로 모든 vertex까지 최단거리를 알 수 있다.

 이렇게 V-1번의 순회로 모든 vertex까지 최단거리를 구했다. 만약 V번째 순회로 dist[x]가 갱신된다면 그 중간에 negative weight cycle이 있다는 것이다.

  • 핵심은 dist[from]이 INF가 아닐 때만 검사해야 한다.

 

 

코드 : O(|V| * |E|)

// edges[i] : i가 edge의 시작인 edge list
// edges[i][0].first : edge list의 edge end
// edges[i][0].second : edge list의 edge weight
vector<int> bellman_ford(int start, vector<vector<pii>> edges){
    vector<long long> dist(V.size(), INF);
    dist[start] = 0;
    
    // V-1번 순회
    for(int i = 0; i< V.size() - 1; i++){
        // 모든 vertex에 대해
        for(int v = 0; v < V.size(); v++){
            // v의 모든 edge에 대해
            for(pii edge : edges[v]){
                int from = v, to = edge.first, weight = edge.second;
                // dist[from]이 INF이면 갱신이 이상하게 될 수도 있음
                if(dist[from] == INF) continue;
                dist[to] = min(dist[to], dist[from] + edge.second);
            }
        }
    }
    
    bool hasNegCycle = false;
    for(int v = 0; v < V.size(); v++){
        for(pii edge : edges[v]){
            int from = v, to = edge.first, weight = edge.second;
            if(dist[from] == INF) continue;
            if(dist[to] > dist[from] + weight){
                hasNegCycle = true;
                break;
            }
        }
    }
    
    if(hasNegCycle) return {-1};
    else return dist;
}

 

 

 

3. Floyd-Warshall : O($|V|^{3}$)

 floyd-warshall은 모든 점에서 모든 점까지의 최단거리를 구한다.


 아래 코드 중 dist는 2차원 배열이며, dist[i][j]는 vertex i에서 vertex j까지 거리이다.

vector<int> V; // V는 vertex vector
vector<vector<int>> dist(V.size(), vector<int>(V.size(), INF)); // dist[i][j] : distance of vertex i to vertex j
for(int i = 0; i<V.size(); i++){
    dist[i][i] = 0;
}

for(int temp = 0; temp < V.size(); temp++){
    for(int from = 0; from < V.size(); from++){
        for(int to = 0; to < V.size(); to++){
            dist[from][to] = min(dist[from][temp] + dist[temp][to], dist[from][to]);
        }
    }
}

 

 

 

4. Dijkstra vs Bellman-Ford vs Floyd-Warshall

dijkstra bellman-ford floyd-warshall
한 점부터 다른 모든 점까지 한 점부터 다른 모든 점까지 모든 점부터 다른 모든 점까지
negative weight edge가 있을 시 사용 불가 negative weight edge가 있어도 사용 가능 negative weight cycle이 있으면 사용 불가