다익스트라 알고리즘(Dijkstra's Algorithm)
1. 최단 경로 문제의 정의
1.1 기본 개념
최단 경로(Shortest Path): 가중치 그래프에서 두 정점 사이의 경로 중 가중치 합이 최소인 경로
경로의 길이: 해당 경로를 구성하는 모든 간선들의 가중치를 합산한 값
예시 그래프:
A ---(3)--- B ---(2)--- C
| | |
(5) (1) (4)
| | |
D ---(2)--- E ---(3)--- F
A에서 F까지의 경로들:
1. A → B → C → F: 3 + 2 + 4 = 9
2. A → D → E → F: 5 + 2 + 3 = 10
3. A → B → E → F: 3 + 1 + 3 = 7 (최단 경로)
1.2 단일-출발점 최단 경로 문제
문제 정의: 주어진 하나의 시작 정점에서 그래프의 다른 모든 정점까지의 최단 경로를 찾는 문제
목표: 시작 정점을 루트로 하는 최단 경로 트리(Shortest Path Tree) 구성
2. 다익스트라 알고리즘의 개념
2.1 알고리즘 개요
개발자: Edsger Dijkstra (1956년)
핵심 아이디어: 탐욕적 접근법을 통해 가장 가까운 정점부터 차례로 방문하며 최단 경로를 확정
전제 조건: 모든 간선의 가중치가 양수여야 함
2.2 프림 알고리즘과의 유사성
공통점:
- 시작 정점에서 점진적으로 확장
- 방문 여부, 거리, 부모 정보 저장
- 탐욕적 선택으로 최적해 보장
차이점:
- 프림: 현재 트리에서 가장 가까운 간선 선택 (MST 구성)
- 다익스트라: 시작점에서 가장 가까운 정점 선택 (최단 경로 구성)
프림 vs 다익스트라:
프림: "현재 트리에 가장 저렴하게 연결할 수 있는 정점은?"
다익스트라: "시작점에서 가장 가까운 미방문 정점은?"
3. 응용 분야
3.1 네트워크 라우팅
인터넷 패킷 라우팅
라우터 네트워크:
- 정점: 라우터
- 간선: 네트워크 연결 (가중치: 지연 시간, 대역폭, 비용)
- 목표: 최소 지연 시간 또는 최소 홉 수로 패킷 전송
실제 적용:
패킷이 서울 → 부산으로 전송될 때
다익스트라로 최적 경로 계산: 서울 → 대전 → 대구 → 부산
최소 홉 라우팅
- 모든 간선 가중치를 1로 설정
- 최소 홉 수를 통한 라우팅
3.2 지도 내비게이션
GPS 네비게이션 시스템
도로 네트워크:
- 정점: 교차로, 지점
- 간선: 도로 구간
- 가중치: 거리, 시간, 비용
다양한 최적화 기준:
1. 최단 거리: 가중치 = 물리적 거리
2. 최단 시간: 가중치 = 예상 소요 시간 (교통 상황 반영)
3. 최소 비용: 가중치 = 통행료 + 유류비
실시간 교통 정보:
- 러시아워: 특정 도로의 가중치 증가
- 사고 발생: 해당 간선 가중치를 매우 크게 설정
3.3 회로 설계
디지털 회로 타이밍 분석
회로 네트워크:
- 정점: 게이트, 플립플롭
- 간선: 신호 연결선 (가중치: 신호 전파 지연)
- 목표: 클럭 신호가 모든 소자에 도달하는 최소 시간 계산
임계 경로 분석:
입력 신호 변화가 출력에 영향을 미치는 최소 시간 계산
3.4 소프트웨어 공학
의존성 분석
모듈 의존성 그래프:
- 정점: 소프트웨어 모듈
- 간선: 의존 관계 (가중치: 컴파일 시간, 복잡도)
- 목표: 변경 사항의 영향 범위 분석
4. 알고리즘 상세 설명
4.1 데이터 구조
각 정점 v에 대해 저장할 정보:
visited[v]: 방문 여부 (boolean)distance[v]: 시작점에서 v까지의 최단 거리parent[v]: 최단 경로에서 v의 이전 정점
4.2 초기화
def initialize(graph, start):
n = len(graph)
visited = [False] * n
distance = [float('inf')] * n
parent = [-1] * n
distance[start] = 0 # 시작점의 거리는 0
return visited, distance, parent
4.3 알고리즘 메인 루프
def dijkstra(graph, start):
n = len(graph)
visited, distance, parent = initialize(graph, start)
for _ in range(n):
# 1. 최소 거리의 미방문 정점 선택
u = find_min_distance_vertex(distance, visited)
if u == -1 or distance[u] == float('inf'):
break # 더 이상 도달 가능한 정점이 없음
# 2. 선택된 정점을 방문 처리
visited[u] = True
# 3. 인접 정점들의 거리 업데이트
for v in range(n):
if not visited[v] and graph[u][v] != 0:
new_distance = distance[u] + graph[u][v]
if new_distance < distance[v]:
distance[v] = new_distance
parent[v] = u
return distance, parent
def find_min_distance_vertex(distance, visited):
min_dist = float('inf')
min_vertex = -1
for v in range(len(distance)):
if not visited[v] and distance[v] < min_dist:
min_dist = distance[v]
min_vertex = v
return min_vertex
5. 상세 실행 예시
5.1 그래프 설정
그래프:
A ---(4)--- B ---(2)--- C
| | |
(2) (1) (3)
| | |
D ---(5)--- E ---(1)--- F
인접 행렬 표현 (A=0, B=1, C=2, D=3, E=4, F=5):
A B C D E F
A [ 0 4 ∞ 2 ∞ ∞ ]
B [ 4 0 2 ∞ 1 ∞ ]
C [ ∞ 2 0 ∞ ∞ 3 ]
D [ 2 ∞ ∞ 0 5 ∞ ]
E [ ∞ 1 ∞ 5 0 1 ]
F [ ∞ ∞ 3 ∞ 1 0 ]
5.2 단계별 실행 (A에서 시작)
초기 상태:
visited = [False, False, False, False, False, False]
distance = [0, ∞, ∞, ∞, ∞, ∞]
parent = [-1, -1, -1, -1, -1, -1]
1단계: A 정점 처리
1. 최소 거리 정점 선택: A (거리 0)
2. A 방문 처리: visited[A] = True
3. A의 인접 정점 업데이트:
- B: distance[B] = min(∞, 0+4) = 4, parent[B] = A
- D: distance[D] = min(∞, 0+2) = 2, parent[D] = A
상태:
visited = [True, False, False, False, False, False]
distance = [0, 4, ∞, 2, ∞, ∞]
parent = [-1, A, -1, A, -1, -1]
2단계: D 정점 처리
1. 최소 거리 정점 선택: D (거리 2)
2. D 방문 처리: visited[D] = True
3. D의 인접 정점 업데이트:
- A: 이미 방문됨
- E: distance[E] = min(∞, 2+5) = 7, parent[E] = D
상태:
visited = [True, False, False, True, False, False]
distance = [0, 4, ∞, 2, 7, ∞]
parent = [-1, A, -1, A, D, -1]
3단계: B 정점 처리
1. 최소 거리 정점 선택: B (거리 4)
2. B 방문 처리: visited[B] = True
3. B의 인접 정점 업데이트:
- A: 이미 방문됨
- C: distance[C] = min(∞, 4+2) = 6, parent[C] = B
- E: distance[E] = min(7, 4+1) = 5, parent[E] = B (더 짧은 경로 발견!)
상태:
visited = [True, True, False, True, False, False]
distance = [0, 4, 6, 2, 5, ∞]
parent = [-1, A, B, A, B, -1]
4단계: E 정점 처리
1. 최소 거리 정점 선택: E (거리 5)
2. E 방문 처리: visited[E] = True
3. E의 인접 정점 업데이트:
- B: 이미 방문됨
- D: 이미 방문됨
- F: distance[F] = min(∞, 5+1) = 6, parent[F] = E
상태:
visited = [True, True, False, True, True, False]
distance = [0, 4, 6, 2, 5, 6]
parent = [-1, A, B, A, B, E]
5단계: C 정점 처리
1. 최소 거리 정점 선택: C (거리 6)
2. C 방문 처리: visited[C] = True
3. C의 인접 정점 업데이트:
- B: 이미 방문됨
- F: distance[F] = min(6, 6+3) = 6 (변화 없음)
상태:
visited = [True, True, True, True, True, False]
distance = [0, 4, 6, 2, 5, 6]
parent = [-1, A, B, A, B, E]
6단계: F 정점 처리
1. 최소 거리 정점 선택: F (거리 6)
2. F 방문 처리: visited[F] = True
3. 모든 정점 방문 완료
최종 상태:
visited = [True, True, True, True, True, True]
distance = [0, 4, 6, 2, 5, 6]
parent = [-1, A, B, A, B, E]
5.3 최단 경로 재구성
def reconstruct_path(parent, start, end):
path = []
current = end
while current != -1:
path.append(current)
current = parent[current]
path.reverse()
return path if path[0] == start else []
# A에서 각 정점까지의 최단 경로:
A → A: [A] (거리: 0)
A → B: [A, B] (거리: 4)
A → C: [A, B, C] (거리: 6)
A → D: [A, D] (거리: 2)
A → E: [A, B, E] (거리: 5)
A → F: [A, B, E, F] (거리: 6)
5.4 최단 경로 트리 시각화
최단 경로 트리 (A가 루트):
A(0)
/ \
B(4) D(2)
/ \
C(6) E(5)
|
F(6)
이 트리는 A에서 시작하여 모든 정점까지의 최단 경로를 나타냄
6. 성능 분석
6.1 시간 복잡도
기본 구현 (인접 행렬):
- 초기화: Θ(|V|)
- 메인 루프: |V|번 반복
- 최소 거리 정점 찾기: O(|V|)
- 인접 정점 업데이트: O(|V|)
- 총 시간 복잡도: Θ(|V|²)
우선순위 큐 최적화:
이진 힙 사용:
import heapq
def dijkstra_optimized(graph, start):
n = len(graph)
distance = [float('inf')] * n
parent = [-1] * n
distance[start] = 0
# (거리, 정점) 형태로 우선순위 큐에 저장
pq = [(0, start)]
visited = [False] * n
while pq:
dist, u = heapq.heappop(pq)
if visited[u]:
continue
visited[u] = True
for v, weight in graph[u]: # 인접 리스트 가정
new_dist = dist + weight
if new_dist < distance[v]:
distance[v] = new_dist
parent[v] = u
heapq.heappush(pq, (new_dist, v))
return distance, parent
- 시간 복잡도: O(|E| log |V|)
- 희소 그래프에서 효율적
피보나치 힙 사용:
- 시간 복잡도: O(|E| + |V| log |V|)
- 이론적으로 가장 효율적
6.2 공간 복잡도
기본 구현: O(|V|)
visited,distance,parent배열
인접 리스트 + 우선순위 큐: O(|V| + |E|)
- 그래프 저장 + 알고리즘 배열
6.3 성능 비교
| 그래프 유형 | 인접 행렬 | 이진 힙 | 피보나치 힙 |
| 밀집 그래프 | Θ(|V|²) | O(|V|² log |V|) | O(|V|²) |
| 희소 그래프 | Θ(|V|²) | O(|E| log |V|) | O(|E| + |V| log |V|) |
선택 기준:
- 밀집 그래프: 인접 행렬이 가장 효율적 (구현도 간단)
- 희소 그래프: 이진 힙 또는 피보나치 힙이 효율적
실제 성능 예시:
- 정점 1000개, 간선 5000개 (희소): 이진 힙 O(5000 log 1000) ≈ O(50,000)
인접 행렬 O(1,000,000) → 이진 힙이 20배 빠름
- 정점 100개, 간선 4950개 (밀집): 이진 힙 O(4950 log 100) ≈ O(33,000)
인접 행렬 O(10,000) → 인접 행렬이 3배 빠름
- 정점 10000개, 간선 20000개 (희소): 피보나치 힙이 이론적으로 최적
7. 알고리즘의 한계와 확장
7.1 한계
양수 가중치 제약:
음수 가중치 예시:
A ---(5)--- B
| |
(-3) (2)
| |
C ---(1)--- D
다익스트라가 실패하는 경우:
A → B → D: 5 + 2 = 7
A → C → D: -3 + 1 = -2 (더 짧지만 발견하지 못함)
이유: A에서 시작하여 B(거리 5)를 먼저 방문하고 확정
나중에 C를 통한 더 짧은 경로를 발견할 수 없음
해결 방안: 벨만-포드 알고리즘 사용
7.2 확장 알고리즘
A* 알고리즘:
- 목적지까지의 휴리스틱 함수 추가
- 더 빠른 탐색 (특정 조건 하에)
- 게임 AI, 로봇 경로 계획에 활용
양방향 다익스트라:
- 시작점과 끝점에서 동시에 탐색
- 만나는 지점에서 최단 경로 구성
- 대용량 그래프에서 효율적
7.3 실무 최적화
조기 종료:
def dijkstra_early_termination(graph, start, target):
# ... 기본 초기화 ...
while pq:
dist, u = heapq.heappop(pq)
if u == target: # 목표 정점에 도달하면 종료
return dist
# ... 나머지 처리 ...
경로 압축:
- 중간 정점이 불필요한 경우 제거
- 메모리 효율성 향상
8. 실무 구현 가이드라인
8.1 자료구조 선택
그래프 표현:
# 인접 리스트 (일반적으로 선호)
graph = {
'A': [('B', 4), ('D', 2)],
'B': [('A', 4), ('C', 2), ('E', 1)],
# ...
}
# 인접 행렬 (밀집 그래프용)
graph = [
[0, 4, inf, 2, inf, inf], # A의 연결
[4, 0, 2, inf, 1, inf], # B의 연결
# ...
]
우선순위 큐 구현:
# Python heapq 사용 (이진 힙)
import heapq
# 거리 업데이트 시 중복 방지
def dijkstra_no_duplicates(graph, start):
distances = {node: float('inf') for node in graph}
distances[start] = 0
pq = [(0, start)]
visited = set()
while pq:
current_dist, u = heapq.heappop(pq)
if u in visited:
continue
visited.add(u)
for neighbor, weight in graph[u]:
distance = current_dist + weight
if distance < distances[neighbor]:
distances[neighbor] = distance
heapq.heappush(pq, (distance, neighbor))
return distances
8.2 에러 처리 및 검증
입력 검증:
def validate_graph(graph):
"""그래프의 유효성 검사"""
for node in graph:
for neighbor, weight in graph[node]:
if weight < 0:
raise ValueError(f"음수 가중치 발견: {node} -> {neighbor}")
if neighbor not in graph:
raise ValueError(f"존재하지 않는 정점: {neighbor}")
def dijkstra_safe(graph, start, target=None):
"""안전한 다익스트라 구현"""
validate_graph(graph)
if start not in graph:
raise ValueError(f"시작 정점이 존재하지 않음: {start}")
# ... 알고리즘 실행 ...
8.3 성능 모니터링
실행 시간 측정:
import time
def timed_dijkstra(graph, start):
start_time = time.time()
result = dijkstra_optimized(graph, start)
end_time = time.time()
print(f"실행 시간: {end_time - start_time:.4f}초")
print(f"정점 수: {len(graph)}, 간선 수: {sum(len(adj) for adj in graph.values())}")
return result
9. 실제 활용 사례
9.1 교통 네비게이션
실시간 교통 정보 반영:
def update_traffic_weights(graph, traffic_data):
"""실시간 교통 정보를 반영하여 가중치 업데이트"""
for road, congestion_level in traffic_data.items():
start, end = road
base_time = graph[start][end]['base_time']
# 혼잡도에 따른 가중치 조정
multiplier = 1 + (congestion_level * 0.5)
graph[start][end]['weight'] = base_time * multiplier
def navigation_routing(graph, start, destination, preferences):
"""사용자 선호도를 고려한 경로 계산"""
if preferences['avoid_tolls']:
# 유료 도로 가중치를 매우 크게 설정
apply_toll_penalty(graph)
if preferences['optimize_for'] == 'time':
# 시간 기준 최적화
weights = 'travel_time'
elif preferences['optimize_for'] == 'distance':
# 거리 기준 최적화
weights = 'distance'
elif preferences['optimize_for'] == 'fuel':
# 연료 효율성 기준
weights = 'fuel_consumption'
return dijkstra_with_weights(graph, start, destination, weights)
9.2 네트워크 라우팅
패킷 라우팅 프로토콜:
class Router:
def __init__(self, router_id):
self.router_id = router_id
self.routing_table = {}
self.network_topology = {}
def update_routing_table(self):
"""다익스트라를 사용하여 라우팅 테이블 업데이트"""
distances, paths = dijkstra_with_paths(
self.network_topology,
self.router_id
)
for destination in distances:
if distances[destination] != float('inf'):
next_hop = paths[destination][1] if len(paths[destination]) > 1 else destination
self.routing_table[destination] = {
'next_hop': next_hop,
'distance': distances[destination]
}
def route_packet(self, packet):
"""패킷을 다음 홉으로 라우팅"""
destination = packet.destination
if destination in self.routing_table:
next_hop = self.routing_table[destination]['next_hop']
return next_hop
else:
return None # 목적지에 도달할 수 없음
9.3 게임 AI
게임 캐릭터 경로 찾기:
def game_pathfinding(game_map, start, goal, character_type):
"""게임에서 캐릭터의 최적 경로 찾기"""
# 캐릭터 타입에 따른 이동 비용 조정
def get_movement_cost(from_tile, to_tile):
base_cost = 1
if character_type == 'infantry':
if to_tile.terrain == 'mountain':
return base_cost * 3
elif to_tile.terrain == 'forest':
return base_cost * 2
elif character_type == 'cavalry':
if to_tile.terrain == 'plain':
return base_cost * 0.5
elif to_tile.terrain == 'mountain':
return base_cost * 5
return base_cost
# 게임 맵을 그래프로 변환
graph = convert_map_to_graph(game_map, get_movement_cost)
# 다익스트라로 최적 경로 계산
distances, paths = dijkstra_with_paths(graph, start)
return paths[goal] if goal in paths else None
10. 마무리
다익스트라 알고리즘은 최단 경로 문제의 핵심 해결책으로, 네트워크 라우팅부터 게임 AI까지 광범위하게 활용됩니다. 양수 가중치라는 제약이 있지만, 실제 대부분의 응용 분야에서 이 조건을 만족하므로 매우 실용적인 알고리즘입니다.
핵심 기억사항:
- 탐욕적 접근: 매 단계에서 가장 가까운 정점을 선택
- 최적 부분 구조: 최단 경로의 부분도 최단 경로
- 성능 최적화: 우선순위 큐 사용으로 효율성 개선
- 실무 적용: 문제에 맞는 가중치 설계가 핵심
올바른 구현과 적절한 최적화를 통해 다양한 실무 문제를 효과적으로 해결할 수 있는 강력한 도구입니다.
'SW > 자료구조' 카테고리의 다른 글
| 블룸 필터(Bloom Filter) (4) | 2025.08.13 |
|---|---|
| 맵(Map) (5) | 2025.08.13 |
| 최소 신장 트리(Minimum Spanning Tree): 프림(Prim's) vs 크루스칼(Kruskal's) 알고리즘 (3) | 2025.07.29 |
| 위상 정렬(Topological Sort) (1) | 2025.07.29 |
| 그래프(Graph) (2) | 2025.07.29 |