Problem Solving/알고리즘

코딩테스트에서 자주 쓰는 C++ STL 라이브러리, 자료구조, 알고리즘 정리(2) - 다익스트라(Dijkstra)

돌돌김 2021. 1. 12. 00:22

Dijkstra

Dijkstra는 비용이 있는 그래프에서 최단 거리를 찾는 알고리즘이다.

최단 거리와 관련된 그래프 알고리즘에는 대표적으로 다음의 3개의 알고리즘도 존재한다.

  • 다익스트라 알고리즘 : 하나의 시작점에 대해 다른 모든 정점들까지의 최단 경로를 구함
  • 벨만포드 알고리즘 : 음의 가중치 고려
  • 플로이드 와샬 알고리즘 : 모든 정점에 대해 다른 모든 정점에 대한 최단경로를 구함

이 중 기본이 되는 다익스트라 알고리즘을 정리해보았다.

참고자료

  • 기본 개념

    • 다익스트라 알고리즘은 하나의 시작점에 대해 최단 경로를 찾는다.

      • 다시 말해 A에서 시작하면 B,C,D,E,F에 대한 최단 경로를 구한다는 것이다.
    • 음의 가중치는 고려하지 않는다.(음의 가중치를 고려한 최단거리 그래프 알고리즘은 벨만포드 알고리즘)

    • 최단거리를 구하기 위해 이전에 구했던 최단거리를 구하고, 갱신이 이루어지므로 DP이기도 하다.

    • 다익스트라 알고리즘에 사용되는 자료구조는 우선순위 큐이다.

    • 우선순위 큐는 기본적으로 최대 힙으로 짜여있기 때문에, 최단거리를 구하기 위해서는 최소 힙으로 만들어 주어야 한다.

      • 최소 힙으로 만드는 방법은 다양하게 있지만(구조체 활용 등), 비용의 값을 음수 화(-) 해서 처리하는 것이 가장 간편하다.
  • 전제 조건

    • 방문 노드와 연결되어 있지 않은 노드까지의 거리는 무한대이다.
  • 자기 자신으로 돌아와서 방문하는 거리는 0이다

  • 시간복잡도

    • O(Elog(V))

      • 정점의 개수 : V

      • 간선의 개수 : E

  • 구현 로직

    • 시작 노드에서 각 노드까지의 최단 거리를 기록하는 배열(일차원) 생성

    • 아직 방문하지 않은 정점들 중 거리가 가장 짧은 정점을 방문

    • 해당 정점에서 인접하고 아직 방문하지 않은 정점들의 거리를 갱신

  • 우선순위 큐 사용자 정의 비교함수 작성
    • 우선순위 큐에 넣어서 비교해야할 요소가 3개 이상 있을 경우, 연산자 오버라이딩을 해야 한다.
    • 이 때, 중요한 것은 단순히 리턴타입이 단순히 bool 인 함수를 만드는 것이 아닌 연산자 오버라이딩이다.
      struct INFO{
        int y;
        int x;
        int z;
      };
      struct cmp{
        bool operator()(INFO a, INFO b){
            if(a.y == b.y){
                if(a.x == b.x){
                    return a.z < b.z; // z는 오름차순
                }
                return a.x > b.x; // x는 오름차순 
            }
            return a.y < b.y; // y는 내림차순
        }
      };
      int main() {
        priority_queue<INFO, vector<INFO>, cmp>pq;
        pq.push({1,2,3});
        pq.push({3,1,2});
        pq.push({4,1,1});
        pq.push({2,4,3});
        pq.push({2,5,3});
        pq.push({2,0,3});
        while(!pq.empty()){
            cout<<pq.top().y << ' '<<pq.top().x<<' '<<pq.top().z <<endl;
            pq.pop();
        }        
        /*
        4 1 1
        3 1 2
        2 0 3
        2 4 3
        2 5 3
        1 2 3
        */
        return 0;
      }
  • 구현코드

    #include<iostream>
    #include<vector>
    #include<queue>
    const int MAX = 20001;
    const int INF = 987654321;
    
    using namespace std;
    int V, E, K;
    vector<pair<int, int>>graph[MAX];
    
    vector<int> dijkstra(int start, int vertex) {
        vector<int>distance(vertex, INF); //start지점을 기준으로 거리
        distance[start] = 0; //자기 자신은 0
        priority_queue<pair<int, int>>pq; //cost, vertex
        pq.push({ 0, start }); // 초기비용과 시작지점
    
        while (!pq.empty()) {
            int cost = -pq.top().first; // 최소값을 저장하기 위해 음수로 저장했으므로 꺼낼때 다시 -를 붙인다.
            int curVertex = pq.top().second; // 현재 방문한 정점
            pq.pop();
    
            if (distance[curVertex] < cost) { // 현재비용이 더 적으면 굳이 더 갈필요가 없다
                continue;
            }
            //curVertex와 연결된 정점 전부 확인
            for (int i = 0; i < graph[curVertex].size(); i++) {
                int neighbor = graph[curVertex][i].first;
                int neighborCost = cost + graph[curVertex][i].second;
    
                // 최소 경로 발견 시 업데이트
                if (distance[neighbor] > neighborCost) {
                    distance[neighbor] = neighborCost;
                    pq.push({ -neighborCost, neighbor }); //최소 값을 꺼내기 위해 음수로 저장
                }
            }
        }
        return distance;
    }
    int main() {
        ios::sync_with_stdio(false);
        cin.tie(NULL);
        cin >> V >> E >> K;
        V++;//정점 번호 : 1부터 시작
        for (int i = 0; i < E; i++) {
            int source, destination, cost;
            cin >> source >> destination >> cost;
            graph[source].push_back({ destination, cost });
        }
        vector<int> result = dijkstra(K, V); // 시작점, 정점의 개수
        for (int i = 1; i < V; i++) {
            if (result[i] == INF) {
                cout << "INF\n";
            }
            else {
                cout << result[i] << "\n";
            }
        }
        return 0;
    }
    
    #include<iostream>
    #include<queue>
    #include<vector>
    #include<string.h>
    #include<algorithm>
    using namespace std;
    int arr[100][100];
    int d[100][100];
    int dy[4] = { 0,0,-1,1 };
    int dx[4] = { 1,-1,0,0 };
    int n;
    
    void dijkstra() {
        memset(d, -1, sizeof(d));
        priority_queue<pair<int, pair<int, int>>>pq; // cost, 노드1, 노드2
        // cost가 낮은 순으로 최소 힙을 구현해야하기 때문에, first에 cost를 놓는다
        pq.push({ 0,{0,0} }); // 시작지점은 0,0
        while (!pq.empty()) {
            int cost = -pq.top().first;
            int y = pq.top().second.first;
            int x = pq.top().second.second;
            pq.pop();
            if (d[y][x] == -1)
                d[y][x] = cost;
            for (int k = 0; k < 4; k++) {
                int ny = y + dy[k];
                int nx = x + dx[k];
                if (0 > nx || nx >= n || 0 > ny || ny >= n)continue;
                int ncost = -cost - arr[ny][nx];
                if (d[ny][nx] == -1)
                    pq.push({ ncost,{ny,nx} });
            }
        }
    }
    int main() {
        /*ios::sync_with_stdio(false);
        cin.tie(0);    cout.tie();*/
        freopen("input.txt", "r", stdin);
        int t=1;
        int c;
        cin >> c;
        while (t<=c) {    
            cin >> n;
            for (int i = 0; i < n; i++) {
                for (int j = 0; j < n; j++) {
                    scanf("%1d", &arr[i][j]); // 입력이 띄어쓰기 없이 01102 처럼 들어올때 씀
                }
            }
            dijkstra();
            cout << "#" << t << ' '<<d[n-1][n-1]<< "\n";
            t++;
        }
    
    }