출처: 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)
'자윤이와고리즘 > Algorithm' 카테고리의 다른 글
서로소 집합 자료구조 (0) | 2020.10.08 |
---|---|
최단 경로 알고리즘:: 플로이드 워셜 알고리즘 (0) | 2020.10.07 |
다이나믹 프로그래밍(dynamic Programming) (0) | 2020.10.04 |
이진탐색 (0) | 2020.10.03 |
그리디 & 구현 (0) | 2020.10.02 |