30강 다익스트라 최단 경로 알고리즘
최단경로 문제
다익스트라 최단 경로 알고리즘
✅특정 노드로부터 각각의 다른 모든 노드로 가는 최단 경로를 모두 계산함
✅현실세계에선 음수의 간선이 없으므로 현실세계에 적용 가능한 알고리즘
✅최단경로 문제는 기본적으로 다이나믹 프로그램 알고리즘으로 분류된다. A에서 C로의 최단경로를 구할 때 B를 거치는 경로가 존재한다면 A-B의 최단경로, B-C의 최단경로가 모두 고려되는 것이기 때문이다. 다만 다익스트라 최단경로 알고리즘은 그리디 알고리즘으로 분류되기도 한다.
다익스트라 최단 경로 알고리즘 동작 과정
✅최단거리 테이블의 각 요소는 해당 번째 노드까지의 최단거리를 기록(지속갱신)
✅최단거리 테이블 초기화 시, 출발노드인 자기 자신은 0 (자기자신에서 자기자신으로 가는 비용은 0이므로), 나머지는 무한
✅3번의 이유로 그리디 알고리즘이라 볼 수 있음
=> 선택 된 노드까지의 최단거리는 바뀌지 않고 선택 순간에 확정되기 때문에 그리디 알고리즘 가능
다익스트라 최단 경로 알고리즘 특징✅ 완벽한 형태의 최단 "경로"
간단한 구현 방법
단계마다 방문하지 않은 노드 중에서 최단 거리가 가장 짧은 노드를 선택하기 위해 매 단계마다 1차원 테이블의 모든 원소를 확인(순차 탐색)한다.
동작 원리
✅ 4번노드까지의 최단거리는 1로 고정되어 바뀌지 않음 => 그리디 가능
✅ 인접노드가 3번일 때 1(4번노드까지의 최단거리)+3(4번노드->3번노드 비용)
✅ 방문하지 않은 노드중에서 최단 거리가 가장 짧은 노드는 2, 5번 노드이지만 편의상 둘 중 작은인덱스부터 선택✅ 인접 노드 중에서 이미 방문한 4번 노드에 대해서는 값이 확정 되었기때문에 계산하지 않아도 된다. (이미 방문 처리된 인접노드는 무시)✅ 이동 가능한 인접노드가 없기도 하지만, 있더라도 이미 다른 모든 노드가 방문되었기때문에 마지막 노드는 무시한다.
소스
import java.util.*;
// 인접한 노드를 의미
class Node {
// 인접한 노드의 인덱스
private int index;
private int distance;
public Node(int index, int distance) {
this.index = index;
this.distance = distance;
}
public int getIndex() {
return this.index;
}
public int getDistance() {
return this.distance;
}
}
public class Main {
public static final int INF = (int) 1e9; // 무한을 의미하는 값으로 10억을 설정
// 노드의 개수(N), 간선의 개수(M), 시작 노드 번호(Start)
// 노드의 개수는 최대 100,000개라고 가정
public static int n, m, start;
// 각 노드에 연결되어 있는 노드에 대한 정보(간선)를 담는 배열
public static ArrayList<ArrayList<Node>> graph = new ArrayList<ArrayList<Node>>();
// 방문한 적이 있는지 체크하는 목적의 배열 만들기, 기본값 false
public static boolean[] visited = new boolean[100001];
// 최단 거리 테이블 만들기
public static int[] d = new int[100001];
//-> graph, visited, d는 0번 인덱스는 사용하지 않음
// 방문하지 않은 노드 중에서, 가장 최단 거리가 짧은 노드의 번호를 반환
public static 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;
}
public static void dijkstra(int start) {
// 시작 노드에 대해서 초기화
d[start] = 0;
visited[start] = true;
for (int j = 0; j < graph.get(start).size(); j++) {
d[graph.get(start).get(j).getIndex()] = graph.get(start).get(j).getDistance();
}
// 시작 노드를 제외한 전체 n - 1개의 노드에 대해 반복
for (int i = 0; i < n - 1; i++) {
// 현재 최단 거리가 가장 짧은 노드를 꺼내서, 방문 처리
int now = getSmallestNode();
visited[now] = true;
// 현재 노드와 연결된 다른 노드를 확인
for (int j = 0; j < graph.get(now).size(); j++) {
int cost = d[now] + graph.get(now).get(j).getDistance();
// 현재 노드를 거쳐서 다른 노드로 이동하는 거리가 더 짧은 경우
if (cost < d[graph.get(now).get(j).getIndex()]) {
d[graph.get(now).get(j).getIndex()] = cost;
}
}
}
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
n = sc.nextInt();
m = sc.nextInt();
start = sc.nextInt();
// 그래프 초기화
for (int i = 0; i <= n; i++) {
graph.add(new ArrayList<Node>());
}
// 모든 간선 정보를 입력받기
for (int i = 0; i < m; i++) {
int a = sc.nextInt();
int b = sc.nextInt();
int c = sc.nextInt();
// a번 노드에서 b번 노드로 가는 비용이 c라는 의미
graph.get(a).add(new Node(b, c));
}
// 최단 거리 테이블을 모두 무한으로 초기화
Arrays.fill(d, INF);
// 다익스트라 알고리즘을 수행
dijkstra(start);
// 모든 노드로 가기 위한 최단 거리를 출력
for (int i = 1; i <= n; i++) {
// 도달할 수 없는 경우, 무한(INFINITY)이라고 출력
if (d[i] == INF) {
System.out.println("INFINITY");
}
// 도달할 수 있는 경우 거리를 출력
else {
System.out.println(d[i]);
}
}
}
}
✅ 상단 그래프 기준 입력예시
6 -> n
11 -> m
1 -> start
-------> 간선(m) 11개 정보
1 2 2
1 3 5
1 4 1
2 4 2
2 3 3
3 2 3
3 6 5
4 3 3
4 5 1
5 3 1
5 6 2
성능 분석
✅ V는 노드의 개수
✅ 선형탐색 O(V) -> getSmallestNode()
✅ N-1(=>dijkstra()) * N(=>getSmallestNode) = O(N^2)
✅ 전체 노드의 개수가 5000개 이하~ : python 기준
개선된 구현 방법
우선순위 큐
힙
✅ 최소 힙 : 값이 낮은 데이터부터 추출(삭제)
✅ 힙은 삽입, 삭제 시 O(logN)만큼의 시간복잡도를 가짐 -> 다양한 알고리즘에서 사용
✅ 리스트로 우선순위 큐 구현한다면
- 삽입 시 데이터를 맨 뒤에 추가하면 되므로 상수시간 O(1) 소요
- 삭제 시 전체 데이터를 순회하며 가장 높은 우선순위 데이터를 찾기 때문에 선형시간 O(N) 소요
✅ 힙정렬 : 최소힙을 통해 배열의 원소들을 힙에 삽입/추출한 결과는 오름차순 형태 (배열의 길이가 N이라면, NlogN(삽입) + NlogN(추출) => O(NlogN))
동작 원리
✅ 갱신여부가 true인 인접 노드만 큐에 (매번 거리 순 정렬하여) 담는다 -> 특정 노드가 여러번 큐에 삽입되어도 처음에 방문처리 된 이후의 노드값은 무시된다.✅ 인접노드 4번은 이미 방문되었기때문에 최단거리가 확정되어서 갱신되지 않음✅ 3번 노드는 이미 방문처리가 되었으므로 최소값으로 확정되었다. 따라서 현재 꺼낸 원소를 무시하도록 한다 -> 방문 테이블 사용하지않고, 현재 최단거리 테이블의 3번 노드 값(3)보다 꺼낸 원소의 거리(4)가 크기때문에 꺼낸 원소를 무시한다.
소스
getSmallestNode() 및 방문테이블을 사용하지 않음
import java.util.*;
class Node implements Comparable<Node> {
private int index;
private int distance;
public Node(int index, int distance) {
this.index = index;
this.distance = distance;
}
public int getIndex() {
return this.index;
}
public int getDistance() {
return this.distance;
}
// 거리(비용)가 짧은 것이 높은 우선순위를 가지도록 설정
@Override
public int compareTo(Node other) {
if (this.distance < other.distance) {
return -1;
}
return 1;
}
}
public class Main {
public static final int INF = (int) 1e9; // 무한을 의미하는 값으로 10억을 설정
// 노드의 개수(N), 간선의 개수(M), 시작 노드 번호(Start)
// 노드의 개수는 최대 100,000개라고 가정
public static int n, m, start;
// 각 노드에 연결되어 있는 노드에 대한 정보를 담는 배열
public static ArrayList<ArrayList<Node>> graph = new ArrayList<ArrayList<Node>>();
// 최단 거리 테이블 만들기
public static int[] d = new int[100001];
public static void dijkstra(int start) {
// 삽입 시 마다 compareTo 기준으로 내부적으로 정렬된다.
PriorityQueue<Node> pq = new PriorityQueue<>();
// 시작 노드로 가기 위한 최단 경로는 0으로 설정하여, 큐에 삽입
pq.offer(new Node(start, 0));
d[start] = 0;
while(!pq.isEmpty()) { // 큐가 비어있지 않다면
// 가장 최단 거리가 짧은 노드에 대한 정보 꺼내기 (compareTo로 구현)
Node node = pq.poll();
int dist = node.getDistance(); // 현재 노드까지의 비용
int now = node.getIndex(); // 현재 노드
// 현재 노드가 이미 처리된 적이 있는 노드라면 무시 (방문테이블 사용 X)
if (d[now] < dist) continue;
// 현재 노드와 연결된 다른 인접한 노드들을 확인
for (int i = 0; i < graph.get(now).size(); i++) {
int cost = d[now] + graph.get(now).get(i).getDistance();
// 현재 노드를 거쳐서, 다른 노드로 이동하는 거리가 더 짧은 경우
if (cost < d[graph.get(now).get(i).getIndex()]) {
d[graph.get(now).get(i).getIndex()] = cost;
pq.offer(new Node(graph.get(now).get(i).getIndex(), cost));
}
}
}
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
n = sc.nextInt();
m = sc.nextInt();
start = sc.nextInt();
// 그래프 초기화
for (int i = 0; i <= n; i++) {
graph.add(new ArrayList<Node>());
}
// 모든 간선 정보를 입력받기
for (int i = 0; i < m; i++) {
int a = sc.nextInt();
int b = sc.nextInt();
int c = sc.nextInt();
// a번 노드에서 b번 노드로 가는 비용이 c라는 의미
graph.get(a).add(new Node(b, c));
}
// 최단 거리 테이블을 모두 무한으로 초기화
Arrays.fill(d, INF);
// 다익스트라 알고리즘을 수행
dijkstra(start);
// 모든 노드로 가기 위한 최단 거리를 출력
for (int i = 1; i <= n; i++) {
// 도달할 수 없는 경우, 무한(INFINITY)이라고 출력
if (d[i] == INF) {
System.out.println("INFINITY");
}
// 도달할 수 있는 경우 거리를 출력
else {
System.out.println(d[i]);
}
}
}
}
성능 분석
✅ 반복문(while문)은 방문처리 된 노드의 경우 continue 되기 때문에 그 아래 로직(while 내의 for문)은 총 노드 개수만큼만 수행된다. 이때 while 내의 for문은 해당 노드의 인접 노드 만큼 확인하기 때문에 while 내의 for문의 총 연산 횟수는 총 간선 수 만큼이다.
✅ 중복 간선을 포함하지 않는 경우 : 두 노드 사이의 존재할 수 있는 간선은 최대 오는 간선, 가는 간선 두개
✅ 노드의 개수를 V 간선의 개수는 V^2 이하의 값일 것이다.
참고
✅ 다른 예제
Author And Source
이 문제에 관하여(30강 다익스트라 최단 경로 알고리즘), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다
https://velog.io/@jhjcoding/30강-다익스트라-최단-경로-알고리즘
저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념
(Collection and Share based on the CC Protocol.)
✅ 다른 예제
Author And Source
이 문제에 관하여(30강 다익스트라 최단 경로 알고리즘), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@jhjcoding/30강-다익스트라-최단-경로-알고리즘저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)