학습 철학 회고
특정 문제의 꼬인 조건에 맞춰진 코드를 달달 외우는 것은 융통성을 잃게 만듭니다. 알고리즘의 본질을 '언어'로 먼저 이해하고, 그것을 순수한 템플릿으로 추상화한 뒤, 실전 문제의 요구사항에 맞춰 조립하는 '설계 주도형 사고(SOP)'를 다익스트라 알고리즘에 적용해 보았습니다.
"하나의 시작점에서 다른 모든 도착점까지 가는 '최단 경로(최소 비용)'를 찾는 길찾기 내비게이션"입니다. 단, 과거로 돌아가는 길(음수 가중치)은 존재하지 않는다는 강력한 전제가 있습니다.
다익스트라는 소문을 퍼뜨리고 메모장을 수정하는 과정과 같습니다.
heapq(우선순위 큐)를 사용하여 탐색 시간을 로 혁신적으로 단축합니다.단순히 코드를 암기하는 것을 넘어, 다익스트라 알고리즘의 무결성을 귀류법(Proof by Contradiction)으로 증명합니다. (전제 조건: 모든 간선의 가중치 )
어떤 문제의 엣지 케이스(Edge Case)에도 오염되지 않은, 가장 범용적인 순수 파이썬 다익스트라 뼈대입니다.
import heapq
def dijkstra_template(start, graph, n):
INF = int(1e9) # 무한대
distance = [INF] * (n + 1) # 메모장 초기화
pq = []
# 힙은 첫 번째 원소 기준으로 정렬되므로 반드시 (비용, 노드) 순으로 삽입
heapq.heappush(pq, (0, start))
distance[start] = 0
while pq:
current_cost, current_node = heapq.heappop(pq)
# [방어 로직] 이미 처리된 적이 있는 노드라면 무시 (지연 삭제)
if distance[current_node] < current_cost:
continue
for weight, next_node in graph[current_node]:
cost = current_cost + weight # 거쳐서 가는 총 비용
# [핵심] 거쳐가는 것이 기존 경로보다 싸다면 (지름길 발견)
if cost < distance[next_node]:
distance[next_node] = cost
heapq.heappush(pq, (cost, next_node))
return distance
distance 배열과 pq를 전역 변수로 두지 않고 함수 내부에 가두어, 여러 번의 테스트 케이스가 주어져도 이전 데이터가 섞이는 치명적인 사이드 이펙트(Side Effect)를 원천 차단했습니다.heapq의 특성을 살려 (비용, 도착점) 순서로 튜플을 구성함으로써, 별도의 정렬 함수 구현 없이 자연스럽게 최소 비용 노드가 추출되도록 설계했습니다.heapq를 사용함으로써, 맹목적인 탐색이 아닌 비용이 최소인 인접 노드를 먼저 탐색하도록 통제하는 점이 가장 중요한 차이점입니다.다익스트라 알고리즘을 설계하다 보면 아주 자연스럽게 다음과 같은 엔지니어링적 의문이 듭니다.
"불필요한 낡은 데이터가 힙(Heap)에 들어가는 것 자체를 사전에 차단하면 큐가 가벼워져서 시간 복잡도를 더 줄일 수 있지 않을까?"
이 의문은 자료구조 최적화의 핵심 쟁점이며, 작성된 템플릿 코드에는 이 딜레마를 해결하기 위한 의도적인 트레이드오프(Trade-off)가 숨어있습니다.
1. 이미 최단 거리가 확정된 노드는 애초에 푸시되지 않는다
"이미 팝(Pop)되어 최단 거리가 고정된 노드는 더 이상 힙에 넣지 말자"는 완벽한 논리는 아래의 단 한 줄의 코드가 이미 수행하고 있습니다.
if cost < distance[next_node]:
어떤 노드가 이미 큐에서 꺼내져(Pop) 최단 거리가 영구 확정되었다면, 그 노드의 distance 메모장 값은 수학적으로 도달 가능한 '절대적 최솟값' 상태입니다. 따라서 나중에 탐색을 돌다가 다른 길을 통해 이 노드로 다시 진입하려 해도, 그 새로운 비용(cost)은 무조건 기존 최솟값보다 크거나 같을 수밖에 없습니다. 결과적으로 조건문이 False가 되어 이미 확정된 노드는 힙에 두 번 다시 푸시되지 않습니다.
2. 자료구조의 물리적 한계와 '지연 삭제(Lazy Deletion)' 전략
그렇다면 힙에 중복된 낡은 데이터가 쌓이는 진짜 이유는 무엇일까요? 바로 '아직 팝되지 않고 힙에서 대기 중인 노드'에 대해 더 짧은 지름길을 발견했을 때 발생합니다.
이때 중복 푸시를 막으려면, 힙 내부를 뒤져서 대기 중이던 낡은 경로 값을 찾아 지우거나 새 값으로 갱신(Decrease-key)해야 합니다.
즉, 중복을 막겠다고 매번 의 막대한 비용을 지불하며 힙을 뒤지는 것보다, 그냥 로 중복 데이터를 밀어 넣는 것이 전체 시스템 관점에서는 압도적으로 효율적입니다. 어차피 힙의 특성상 값이 더 작은 '진짜 최단 경로'가 먼저 팝되어 메모장을 갱신할 것입니다.
이후 힙 구석에 방치되어 있던 '낡은 데이터'가 뒤늦게 팝되어 나오면, 우리는 이미 준비해 둔 방어 로직(if distance[current_node] < current_cost: continue)을 통해 단 의 비용으로 무시하고 폐기(Discard)해 버리면 됩니다. 이것이 바로 컴퓨터 과학에서 널리 쓰이는 지연 삭제(Lazy Deletion) 아키텍처입니다.