출처: www.youtube.com/watch?v=acqm9mM1P6o

 

<최단 경로 문제>

- 최단 경로 알고리즘은 가장 짧은 경로를 찾는 알고리즘

- 다양한 문제 상황

    1. 한 지점에서 다른 한 지점까지의 최단 경로

    2. 한 지점에서 다른 모든 지점까지의 최단 경로

    3. 모든 지점에서 다른 모든 지점까지의 최단 경로

- 각 지점(국가, 마을, 도시등)은 그래프에서 노드로 표현

- 지점 간 연결된 도로는 그래프에서 간선으로 표현

- 기본적으로 dp로 분류됨


<다익스트라 최단 경로 알고리즘>

- 특정한 노드에서 출발하여 다른 모든 노드로 가는 최단 경로를 계산한다.

- 다익스트라 최단 경로 알고리즘은 음의 간선이 없을 때 정상적으로 동작

    - 현실 세계의 도로(간선)은 음의 간선으로 표현되지 않는다.

- 다익스트라 최단 경로 알고리즘은 그리디 알고리즘으로 분류

    - 매 상황에서 가장 비용이 적은 노드를 선택해 임의의 과정을 반복

 

<다익스트라 최단 경로 알고리즘 동작 과정>

더보기

1. 출발 노드 설정(하나의 출발 노드로부터 다른 모든 노드를 구하는 문제이므로)

2. 최단 거리 테이블 초기화(무한으로 설정, 자기자신은 0(자기자신 -> 자기자신 = 0))

3. 방문하지 않은 노드 중에서 최단 거리가 가장 짧은 노드를 선택(그리디 알고리즘, 방문x노드 중 최단 거리가 짧은걸 선택할때마다 해당 최단거리는 바뀌지 않음)

4. 해당 노드를 거쳐 다른 노드로 가는 비용을 계산하여 최단 거리 테이블을 갱신

5. 위 과정에서 3, 4번을 반복

- 알고리즘 동작 과정에서 최단 거리 테이블각 노드에 대한 현재까지의 최단 거리 정보를 가지고 있음

- 처리 과정에서 더 짧은 경로를 찾으면 '이제부터는 이 경로가 제일 짧은 경로야'라고 갱신

 

현재값, 갱신된 값, 갱신 여부

 

<특징>

- 그리디 알고리즘: 매 상황에서 방문하지 않은 가장 비용이 적은 노드를 선택해 임의의 과정 반복

- 단계를 거치며 한 번 처리된 노드의 최단 거리는 고정되어 더 이상 바뀌지 않음

    : 한 단계당 하나의 노드에 대한 최단 거리를 확실히 찾는 것으로 이해

- 다익스트라 알고리즘을 수행한 뒤에 테이블에 각 노드까지의 최단 거리 정보가 저장

    : 완벽한 형태의 최단 경로를 구하려면 소스코드에 추가적인 기능을 더 넣어야 한다.

 

<간단한 구현 방법>

1. 단계마다 방문하지 않은 노드 중에서 최단 거리가 가장 짧은 노드를 선택하기 위해 매 단계마다 1차원 테이블의 모든 원소를 확인(순차탐색)한다.

#python dijkstra
import sys
input = sys.stdin.readline
INF = int(1e9) # 무한을 의미, 10억 설정

n, m = map(int, input().split()) #노드 개수, 간선개수 입력 받기

start = int(input()) # 시작 노드 번호 입력 받기

graph = [[] for i in range(n+1)] # 각 노드에 연결되어 있는 노드에 대한 정보를 담는 리스트(연결리스트 형태로 그래프 초기화)

visited = [False] * (n+1)# 방문한 적이 있는지 체크하는 목적의 리스트 만들기

distance = [INF] * (n+1) # 최단거리 테이블을 모두 무한으로 초기화

for _ in range(m): # 모든 간선 정보 입력
    a, b, c = map(int, input().split())
    
    graph[a].append((b, c)) # a번 노드에서 b번 노드로 가는 비용이 c라는 의미(묶어서 넣어준다)

# 방문하지 않은 노드 중에서, 가장 최단 거리가 짧은 노드 반환
def get_smallest_node():
    min_value = INF
    index = 0 # 가장 최단 거리가 짧은 노드(인덱스)
    for i in range(1, n+1):
        if distance[i] < min_value and not visited[i]:
            min_value = distance[i]
            index = i
     return index
     
 def dijkstra(start):
     # 시작 노드에 대해 초기화
     distance[start] = 0 
     visited[start] = True
     for j in graph[start]:
         distance[j[0]] = j[1]
         
     for i in range(n-1): #시작 노드를 제외한 전체 n-1개의 노드에 대해 반복
         now = get_smallest_node() # 현재 최단 거리가 가장 짧은 노드를 꺼내서, 방문 처리
         visited[now] = True
         for j in graph[now]: #현재 노드와 연결된 다른 노드 확인
             cost = distance[now] + j[1]
             if cost < distance[j[0]]: #현재 노드를 거쳐 다른 노드로 이동하는 거리가 더 짧은 경우
                 distance[j[0]] = cost

#다익스트라 알고리즘 수행
dijkstra(start)

#모든 노드로 가기 위한 최단 거리 출력
for i in range(1, n+1):
    if distance[i] == INF: #도달할 수 없는 경우, 무한 출력
        print("INFINITY")
    else:
        print(distance[i]) #도달 가능한 경우 거리 출력
#c++ dijkstra

int n, m, start;
vector<pair<int, int>> graph[100001]; //각 노드에 연결되어 있는 노드에 대한 정보를 담은 배열

bool visited[100001]; //방문한 적이 있는지 체크하는 목적의 배열 만들기
int d[100001]; //최단거리 테이블

//방문하지 않은 노드 중에서, 가장 최단 거리가 짧은 노드의 번호를 반환
int getSmallestNode(){
    int min_value = INF;
    int index = 0;
    for(int i = 1; i <=n; i++){
        if(d[i] < min_value && !visited[i]){
            min_value = d[i];
            index = i;
        }
    }
    return index;
}

void dijkstra(int start){
    //시작 노드에 대해 초기화
    d[start] = 0;
    visited[start] = true;
    for(int j=0; j < graph[start].size(); j++)
        d[graph[start][j].first] = graph[start][j].second;
    // 시작 노드를 제외한 전체 n-1개의 노드에 대해 반복
    for(int i=0;i < n-1; i++){
        //현재 상태를 기준으로, 최단 거리가 가장 짧은 노드를 꺼내서 방문처리
        int now = getSmallestNode();
        visited[now] = true;
        //현재 노드와 연결된 다른 노드를 확인
        for(int j=0; j < graph[now].size(); j++){
            int cost = d[now] + graph[now][j].second;
            //현재 노드를 거쳐서 다른 노드로 이동하는 거리가 더 짧은 경우
            if (cost < d[graph[now][j].first]) 
                d[graph[now][j].first] = cost; //갱신
         }
     }
 }
 
 int main(){
     cin >> n >> m >> start;
     
     //모든 간선 정보 입력
     for(int i=0; i < m ; i++){
         int a, b, c;
         cin >> a>> b>> c;
         //a번 노드에서 b번 노드로 가는 비용이 c라는 의미
         graph[a].push_back({b, c})
     }
     
     
     //최단 거리 테이블을 모두 무한으로 초기화
     fill_n(d, 100001, INF);
     //알고리즘 수행
     dijkstra(start);

     for(int i= 1; i <=n; i++){
         if (d[i] == INF)
             cout << "INFINITY"<<endl;
         else
             cout << d[i] << endl;
     }
}

 

<다익스트라 알고리즘: 간단한 구현 방법 성능 분석>

- 총 O(V)번에 걸쳐서 최단 거리가 가장 짧은 노드를 매번 선형 탐색해야 함

- 따라서 전체 시간 복잡도는 O(V2)

- 일반적으로 코딩 테스트의 최단 경로 문제에서 전체 노드의 개수가 5000개 이하라면 이 코드로 문제 해결 가능

    : 노드의 개수가 10000개가 넘어간다면???? 1억번 이상의 연산일텐데.....

--> 이를 위해 우선순위 큐 자료구조를 이용한다!

 

<우선순위 큐(Priority Queue)>

- 우선순위가 가장 높은 데이터를 가장 먼저 삭제하는 자료구조

- 예를 들어 여러 개의 물건 데이터를 자료구조에 넣었다가 가치가 높은 물건 데이터부터 꺼내서 확인해야 하는 경우 우선순위 큐 이용 가능

 

<힙(Heap)>

- 우선순위 큐(Priority Queue)를 구현하기 위해 사용하는 자료구조 중 하나

- 최소 힙(Min Heap, 값이 낮은 데이터부터 꺼냄)과 최대 힙(Max Heap, 값이 높은 데이터 부터 꺼냄)이 있음

[리스트로 우선순위 큐 구현]

- 삽입 시간 O(1), 삭제 시간 O(N)

[힙으로 우선순위 큐 구현]

- 삽입 시간 O(log N), 삭제 시간 O(log N)

 

[파이썬 힙 라이브러리]

최악의 경우에도 O(NlogN)보장

# python 최소 힙

import heapq

#오름차순 힙 정렬
def heapsort(iterable):
    h = []
    #모든 원소를 차례대로 힙에 삽입
    for value in iterable:
        heapq.heappush(h, value)
        # heapq.heappush(h, -value) #최대 힙
    
    #힙에 삽입된 모든 원소를 차례대로 꺼내어 담기
    for i in range(len(h)):
        result.append(heapq.heappop(h))
        # result.append(-heapq.heappop(h)) #최대 힙
    return result


result = heapsort([1, 3, 5, 7, 9, 2, 4, 6, 8, 0])
print(result) #[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

- 단계마다 방문하지 않은 노드 중에서 최단 거리가 가장 짧은 노드를 선택하기 위해 힙 자료구조를 이용

- 다익스트라 알고리즘이 동작하는 기본 원리는 동일하다

    : 현재 가장 가까운 노드를 저장해 놓기 위해서 힙 자료구조를 추가적으로 이용한다는 점이 다름

    : 현재의 최단 거리가 가장 짧은 노드를 선택해야하므로 최소 힙을 사용

[우선순위 큐를 사용한 다익스트라 알고리즘 동작 과정]

더보기

[초기 상태] 그래프를 준비, 출발 노드 설정, 우선 순위큐에 삽입

우선순위 큐에서 원소를 꺼내, 방문 처리 후 최단 거리 테이블 갱신

 

<개선된 다익스트라>

#python dijkstra, developed

import heapq
import sys
input = sys.stdin.readline
INF = int(1e9)

n, m = map(int, input().split()) # 노드 개수, 간선 개수

start = int(input()) #시작 노드

graph = [[] for i in range(n+1)] #각 노드에 연결 되어 있는 노드에 대한 정보를 담는 리스트
distance = [INF] * (n+1)#최단 거리 테이블을 무한으로 초기화

for _ in range(m): #모든 간선 정보 입력
    a, b, c = map(int, input().split())
    graph[a].append((b, c)) #a->b, 비용 c
    
def dijkstra(start):
    q = []
    heapq.heappush(q, (0, start)) #시작 노드로 가기 위한 최단 거리는 0으로 설정하여, 큐에 삽입
    distance[start] = 0
    while q: #큐가 비어있지 않다면
        dist, now = heapq.heappop(q) #가장 최단 거리가 짧은 노드에 대한 정보 꺼내기
        if distance[now] < dist: #현재 노드가 이미 처리된 적 있는 노드라면 무시(방문 테이블을 따로 두지 않음)
            continue
        for i in graph[now]: #현재 노드와 연결된 다른 인접한 노드들 확인
            cost = dist + i[1]
            #현재 노드를 거쳐서, 다른 노드로 이동하는 거리가 더 짧은 경우
            if cost < distance[i[0]]:
                distance[i[0]] = cost
                heapq.heappush(q, (cost, i[0]))

dijkstra(start)

for i in range(1, n+1):
    if distance[i] == INF:
        print("infinity")
    else:
        print(distance[i])

 

//c++ dijkstra, developed
#define INF 1e9

int n, m, start;
vector<pair<int, int>> graph[100001]; //간선 정보
int d[100001]; //최단 거리 테이블

void dijkstra(int start){
    priority_queue<pair<int, int>> pq; //우선순위 큐(최소 비용 값, 노드번호), 최대힙으로 구성되어있음
    
    pq.push({0, start}); //시작 노드로 가기위한 최단 경로는 0으로 설정하여 큐에 삽입
    d[start] = 0;
    while(!pq.empty()){
        //가장 최단 거리가 짧은 노드에 대한 정보 꺼내기
        int dist = -pq.top().first; //현재 노드까지의 비용, 최대힙으로 구성되어 있기 때문에 -
        int now = pq.top().second; //현재 노드
        
        pq.pop();
        
        if(d[now] < dist) continue; //현재 노드가 이미 처리된 적 있는 노드라면 무시
        
        //현재 노드와 연결된 다른 인접한 노드들을 확인
        for(int i= 0; i < graph[now].size(); i++){
            int cost = dist + graph[now][i].second;
            //햔재 노드를 거쳐서, 다른 노드로 이동하는 거리가 더 짧은 경우
            if( cost < d[graph[now][i].first] ) {
                d[graph[now][i].first] = cost;
                pq.push(make_pair(-cost, graph[now][i].first));//최대힙으로 구성되어 있기 때문
            }
        }
    }
}
                
           
int main(){
    cin >> n >> m >> start;
    //모든 간선 정보 입력
    for(int i=0;i < m; i++){
        int a, b, c;
        cin >> a>> b>> c;
        graph[a].push_back({b,c });   
    }
    
    fill(d, d+100001, INF);
    dijkstra(start);
    
    for(int i =1; i <= n; i++){
        if(d[i] == INF)
            cout << "infinity\n";
        else
            cout << d[i] <<endl;       
    }       
}
                

[개선된 구현 방법 성능 분석]

- 시간복잡도 O(ElogV)

- 노드를 하나씩 꺼내 검사하는 반복문은 노드의 개수 V이상의 횟수로는 처리되지 않음

    : 결과적으로 현재 우선순위 큐에서 꺼낸 노드와 연결된 다른 노드들을 확인하는 총횟수는 최대 간선의 개수(E)만큼 연산이 수행될 수 있음

- 직관적으로 전체 과정은 E개의 원소를 우선순위 큐에 넣었다가 모두 빼내는 연산과 매우 유사

    : 시간복잡도를 O(ElogE)로 판단할 수 있다.

    : 중복 간선을 포함하지 않는 경우에 이를 O(ElogV)로 정리할 수 있음

        : O(ElogE) -> O(ElogV2) -> O(2ElogV) -> O(ElogV)

 

 

 

+ Recent posts