다익스트라알고리즘

[ CS > Algorithm ]

[알고리즘] 다익스트라 - 미로 탈출

 Carrot Yoon
 2025-09-19
 17

다익스트라 알고리즘

이 문서에서는 다익스트라 알고리즘의 개념과 작동 방식과 프로그래머스의 [미로 탈출] 문제를 설명해요.

다익스트라 알고리즘은 그래프의 한 정점에서 다른 모든 정점까지의 최단 경로를 찾는 알고리즘이에요.

컴퓨터 네트워크, 내비게이션 등 경로 탐색이 필요한 여러 분야에서 사용할 수 있어요.

1. 기본 개념

다익스트라 알고리즘은 다음과 같은 핵심 개념을 기반으로 해요.

  • 가중치 그래프 : 간선마다 음수가 아닌 가중치가 존재하는 그래프

  • 출발점 : 최단 경로를 시작할 노드

  • 거리 배열 : 각 노드 사이의 최단 거리

  • 우선순위 큐 : 방문할 노드 중 가장 누적 비용이 적은 순으로 정렬하는 큐

정점의 개수를 V, 간선의 개수를 E라고 할 때, 다익스트라 알고리즘의 시간 복잡도는 다음과 같이 계산해요.

  • 우선순위 큐를 사용하는 경우 시간복잡도는 O(VlogV + ElogV)에요.

    • 각 반복문마다 미방문 노드 중 출발점으로부터 현재까지 누적 최단 거리를 우선순위 큐로 갱신하는 데 O(VlogV) 시간이 걸려요.

    • 각 노드마다 이웃한 노드의 최단 거리를 갱신하는 데 O(ElovV) 시간이 걸려요.

만약 음의 가중치가 존재하는 경우 벨만-포드 알고리즘을 사용 해야해요.

2. 작동 방식

다익스트라 알고리즘은 보통 다음의 절차를 따라요.

  1. 거리 배열 초기화

    • 출발 노드의 거리는 0, 나머지 노드는 무한대로 설정해요.

image.webp
  1. 가장 가까운 노드 선택

    • 큐의 출발점과 인접한 노드들을 선택하여 큐에 추가해요.

image.webp
  1. 거리 갱신

    • 선택된 노드의 인접 노드를 확인하면서, 출발점으로 부터 방문하지 않은 경로가 발견되면 계속 거리 배열 갱신을 해요.

image.webpimage.webpimage.webpimage.webpimage.webp
  1. 모든 노드가 방문되거나, 더 이상 갱신할 경로가 없을 때 알고리즘 종료해요.

image.webp

이 과정에서 더 짧은 누적거리를 가진 경로를 먼저 탐색하기 위해 우선순위 큐(Priority Queue)를 사용하여, 먼저 방문한 노드의 누적 거리 값이 최단 거리 값이에요.

3. 우선순위 큐

다익스트라 알고리즘은 우선순위 큐가 아니더라도 계속 sort 함수를 실행하여 구현할 수 있어요. 하지만 시간복잡도가 좋지 않기 때문에 우선순위 큐를 활용해야 해요. 그래서 자바스크립트에서 효율적인 우선순위 큐를 구현할 수 있어야 해요. 우선순위 큐 예시 코드를 보여드릴 게요.

class PriorityQueue {  constructor(comparator = (a, b) => a > b) {    this.heap = [];    this.comparator = comparator; // boolean comparator  }  size() { return this.heap.length; }  isEmpty() { return this.size() === 0; }  peek() { return this.heap[0]; }  push(value) {    this.heap.push(value);    this._heapifyUp();  }  pop() {    if (this.isEmpty()) return null;    const top = this.peek();    const last = this.heap.pop();    if (!this.isEmpty()) {      this.heap[0] = last;      this._heapifyDown();    }    return top;  }  _heapifyUp() {    let index = this.size() - 1;    const element = this.heap[index];    while (index > 0) {      const parentIndex = Math.floor((index - 1) / 2);      const parent = this.heap[parentIndex];      if (!this.comparator(element, parent)) break;      this.heap[index] = parent;      index = parentIndex;    }    this.heap[index] = element;  }  _heapifyDown() {    let index = 0;    const length = this.size();    const element = this.heap[0];    while (true) {      const leftIndex = 2 * index + 1;      const rightIndex = 2 * index + 2;      let swapIndex = null;      if (leftIndex < length && this.comparator(this.heap[leftIndex], element)) {        swapIndex = leftIndex;      }      if (        rightIndex < length &&        ((swapIndex === null && this.comparator(this.heap[rightIndex], element)) ||         (swapIndex !== null && this.comparator(this.heap[rightIndex], this.heap[leftIndex])))      ) {        swapIndex = rightIndex;      }      if (swapIndex === null) break;      this.heap[index] = this.heap[swapIndex];      index = swapIndex;    }    this.heap[index] = element;  }}

4. 실전 예제 - [프로그래머스]미로 탈출

image.webp

미로 탈출 문제에서 어떻게 다익스트라 알고리즘을 이용하여 문제를 풀었는지 코드를 보여드릴게요. 문제 설명은 길어서 별도 링크를 첨부할게요.

  • 문제 설명 : 출발지에서 목적지까지 갈 수 있는 최단 거리 값을 구해야 해요. 노드는 일반 노드와 함정 노드가 있어요.

  • 문제 핵심 :

    • 다익스트라 알고리즘으로 출발지 X에서 목적지 Y까지의 거리를 구해야해요.

    • 간선에는 방향이 존재해요.

    • A노드에서 B노드 사이에는 여러 개의 간선이 존재할 수 있어요.

    • 노드에는 함정인 노드가 존재하고, 함정인 노드에 도착하면 함정 노드에 연결된 모든 간선의 방향이 반대로 바껴요.

    • 출발지와 도착지는 함정 노드가 아니에요.

    • 항상 출발지부터 도착지까지 도착이 가능해요.

  • 풀이 핵심:

    • 출발지 X로부터 전체 노드의 거리를 구하는 것이 아닌 특정 X 노드와의 거리만 구하면 종료해야해요.

    • A노드와 B노드 사이의 간선은 방향별로 최소값인 간선만 남겨야해요.

    • 도착지가 없는 경우는 생각하지 않아도 되요.(못찾는 예외 처리가 필요 없어요.)

    • 함정이 있기 때문에 같은 노드를 2번 밟는 경우가 생겨요.

    • 간선이 바뀌는 노드들을 알기위해 노드, 간선 정보에 더해서 추가적인 상태값 저장이 필요해 보여요.

아래는 저가 처음 정답으로 제출한 코드에요. 급하게 푸느라 변수명부터 모두 엉망이네요. 위의 [3. 우선순위 큐]와 아래 코드의 우선순위 큐의 다른 점은 literal object로 구현했어요. 속도는 두 방법 모두 비슷하게 나오긴 했는데, V8 엔진 최적화에서 literal object가 더 안좋을 수 있다고 해요.(아마도 V8 엔진의 최적화 방식에 따라 같은 타입의 반복이 아니기 때문에 타입 템플릿 캐싱이 일어나지 않을 것이라 개인적으로 추측해요. 틀릴 수도 있으니 직접 학습해 보세요.)

class PriorityQueue {  constructor(oper) {    this.items = {};    this.tail = 0;    this.oper = oper;  }  push(item) {    const pushIndex = this.tail++;    this.items[pushIndex] = item;    this.heapUp(pushIndex);  }  pop() {    if (this.isEmpty()) return undefined;    const item = this.items[0];    const lastIndex = --this.tail;    [this.items[0], this.items[lastIndex]] = [this.items[lastIndex], this.items[0]];    delete this.items[lastIndex];    if (!this.isEmpty()) this.heapDown(0);    return item;  }  isEmpty() {    return 0 === this.tail;  }  size() {    return this.tail;  }  getParentIndex(index) {      const parentIndex = Math.floor((index-1)/2);       if(parentIndex < 0) return null;      return parentIndex;  }  swapParent(index) {      const parentIndex = Math.floor((index-1)/2);       if(parentIndex < 0) return null;      [this.items[index], this.items[parentIndex]] = [this.items[parentIndex],this.items[index]]  }  heapUp(index) {    const item = this.items[index];    const parentIndex = this.getParentIndex(index)    if(parentIndex === null) return;    const parent = this.items[parentIndex];    if(this.oper(item, parent)){        this.swapParent(index);        this.heapUp(parentIndex);    }  }  heapDown(index) {      const item = this.items[index];      const lIndex = index*2+1;      const rIndex = index*2+2;      if(this.tail <= lIndex) return;      if(this.tail <= rIndex) {          if(this.oper(this.items[lIndex],item))              [this.items[lIndex], this.items[index]] = [this.items[index], this.items[lIndex]]          return this.heapDown(lIndex);      }      if(this.oper(this.items[lIndex],this.items[rIndex])) {          if(this.oper(this.items[lIndex],item)) {              [this.items[lIndex], this.items[index]] = [this.items[index], this.items[lIndex]]              return this.heapDown(lIndex);          }      } else {           if(this.oper(this.items[rIndex],item)) {              [this.items[rIndex], this.items[index]] = [this.items[index], this.items[rIndex]]               return this.heapDown(rIndex);          }      }  }}// [현재 위치, 이동 거리]function solution(n, start, end, roads, traps) {    const MAP = Array.from(Array(n+1),()=>Array(n+1).fill(0))    const visited = Array.from(Array(n+1),()=>[Math.infinite, Math.infinite])    roads.forEach((road)=>{        if(MAP[road[0]][road[1]] === 0)            MAP[road[0]][road[1]]=road[2]        else if(MAP[road[0]][road[1]] > road[2])            MAP[road[0]][road[1]] = road[2];    })        const queue = new PriorityQueue((item1,item2) => (item1[1] < item2[1]));    queue.push([start,0,0,{},{}])    const trapsDict = traps.reduce((prev, trap)=>({...prev,[trap]:true}), {})    const answer = dik(queue, start, end, MAP, visited, trapsDict);    return answer;}function dik(queue, start, end, Map, visited,traps) {    const current = queue.pop(); // [위치, 이동 거리, 횟수, trapped]    const [pos,dis,count,trapped,visited2] = current;        const isTrap = traps[pos];    if(pos === end) return dis;    if(count >= 2)  return dik(queue, start, end, Map, visited, traps);    if(dis > visited[pos][count]) return dik(queue, start, end, Map, visited, traps);    visited[pos][count] = dis;        const visitedCopy = {...visited2}    if(visitedCopy[pos]) {        visitedCopy[pos] += 1;    } else {        visitedCopy[pos] = 1;    }        const newTrapped = {...trapped};    if(isTrap){        newTrapped[pos] =  newTrapped[pos]? 2: 1    }    const newMap2 = [...Map[pos]];    if(isTrap && newTrapped[pos]!==2) {        for(let i=1; i<newMap2.length; i++) {            newMap2[i] = Map[i][pos];                    }    }    Object.keys(trapped).forEach((trap)=> {        if(newMap2[trap] === Map[trap][pos])            newMap2[trap] = Map[pos][trap];        else             newMap2[trap] = Map[trap][pos];    })    newMap2.forEach((to,index)=> {        if(to) {            const visitTrapCount = newTrapped[index] ?? 0;            const visitCount = visitedCopy[index] ?? 0;            if(visitTrapCount>1) return;            if(visitCount >1) return;            queue.push([index,to+dis,visitCount,newTrapped,visitedCopy])                                }    })        return dik(queue, start, end, Map, visited, traps);}

위 코드를 더 깔끔하게 변수명이랑 바꿔볼게요.

class PriorityQueue {  constructor(comparator = (a, b) => a > b) {    this.heap = [];    this.comparator = comparator;  }  size() { return this.heap.length; }  isEmpty() { return this.size() === 0; }  peek() { return this.heap[0]; }  push(value) {    this.heap.push(value);    this._heapifyUp();  }  pop() {    if (this.isEmpty()) return null;    const top = this.peek();    const last = this.heap.pop();    if (!this.isEmpty()) {      this.heap[0] = last;      this._heapifyDown();    }    return top;  }  _heapifyUp() {    let index = this.size() - 1;    const element = this.heap[index];    while (index > 0) {      const parentIndex = Math.floor((index - 1) / 2);      const parent = this.heap[parentIndex];      if (!this.comparator(element, parent)) break;      this.heap[index] = parent;      index = parentIndex;    }    this.heap[index] = element;  }  _heapifyDown() {    let index = 0;    const length = this.size();    const element = this.heap[0];    while (true) {      const leftIndex = 2 * index + 1;      const rightIndex = 2 * index + 2;      let swapIndex = null;      if (leftIndex < length && this.comparator(this.heap[leftIndex], element)) {        swapIndex = leftIndex;      }      if (        rightIndex < length &&        ((swapIndex === null && this.comparator(this.heap[rightIndex], element)) ||         (swapIndex !== null && this.comparator(this.heap[rightIndex], this.heap[leftIndex])))      ) {        swapIndex = rightIndex;      }      if (swapIndex === null) break;      this.heap[index] = this.heap[swapIndex];      index = swapIndex;    }    this.heap[index] = element;  }}// [현재 위치, 누적 거리, 방문 횟수, 활성화된 트랩 상태, 방문 기록]function solution(n, start, end, roads, traps) {  const graph = Array.from({ length: n + 1 }, () => Array(n + 1).fill(0));  // [처음 노드 밟았을 때 최단 거리, 2번째 노드 밟았을 때 최단 거리]  const distanceRecord = Array.from({ length: n + 1 }, () => [Infinity, Infinity]);  // 그래프 초기화  roads.forEach(([from, to, cost]) => {    if (graph[from][to] === 0 || graph[from][to] > cost) {      graph[from][to] = cost;    }  });  const pq = new PriorityQueue((a, b) => a[1] < b[1]);  pq.push([start, 0, 0, {}, {}]);  const trapSet = traps.reduce((acc, trap) => ({ ...acc, [trap]: true }), {});  return dijkstra(pq, end, graph, distanceRecord, trapSet);}function dijkstra(pq, end, graph, distanceRecord, trapSet) {  while (!pq.isEmpty()) {    const current = pq.pop();    const [pos, totalDist, visitCount, activeTraps, visitHistory] = current;    const isTrap = trapSet[pos];    if (pos === end) return totalDist;    if (visitCount >= 2) continue;    if (totalDist > distanceRecord[pos][visitCount]) continue;    distanceRecord[pos][visitCount] = totalDist;    // 방문 기록 업데이트    const newVisitHistory = { ...visitHistory };    newVisitHistory[pos] = (newVisitHistory[pos] ?? 0) + 1;    // 트랩 활성화 상태 업데이트    const newActiveTraps = { ...activeTraps };    if (isTrap) {      newActiveTraps[pos] = newActiveTraps[pos] ? 2 : 1;    }    // 현재 위치 기준 이동 가능한 간선들    const nextEdges = [...graph[pos]];    if (isTrap && newActiveTraps[pos] !== 2) {      for (let i = 1; i < nextEdges.length; i++) {        nextEdges[i] = graph[i][pos];      }    }    // 다른 트랩 영향 반영    Object.keys(activeTraps).forEach((trap) => {      if (nextEdges[trap] === graph[trap][pos]) {        nextEdges[trap] = graph[pos][trap];      } else {        nextEdges[trap] = graph[trap][pos];      }    });    // 이동    nextEdges.forEach((cost, nextPos) => {      if (cost) {        const trapVisitCount = newActiveTraps[nextPos] ?? 0;        const nodeVisitCount = newVisitHistory[nextPos] ?? 0;        if (trapVisitCount > 1) return;        if (nodeVisitCount > 1) return;        pq.push([nextPos, totalDist + cost, nodeVisitCount, newActiveTraps, newVisitHistory]);      }    });  }  return -1; // 도달 불가능}

위 코드에서 왜 이렇게 바꿨는지, 왜 안바꿨는지 해설을 추가해볼게요.

  • 위 코드에서 다익스트라 알고리즘 설명과 다르게 구현한 부분이 있을까요??

    • 네 애초에 이미 방문한 노드면 원래 우선순위 큐에 해당 노드를 안넣는게 더 빠를거라 생각합니다. 하지만 저는 함정 때문에 2번 방문하는 경우까지 생각하다 보니 미쳐 생각하지 못했던 것 같습니다. 여러분은 distanceRecord[nodeVisitCount][nextPos]가 이미 방문되었다면 아에 queue에 안넣게 구현하세요!! 그게 더 빠를거라 생각합니다.

  • activeTraps를 왜 별도로 함께 넘기나요?

    • newVisitHistory와 traps 정보만 가지고 발동된 함정들을 알 수 있지만, 발동된 함정을 찾는데 시간이 많이 거릴 수 있다고 생각해서 별도 Map 자료형으로 activeTraps를 갖도록 했습니다.

  • 그래프 간선 정보를 N X M 배열로 갖는데 너무 메모리를 많이 차지 않을까요?

    • 네 노드의 개수가 증가하면 메모리를 많이 차지합니다. 그래서 Map 자료형으로 노드 별로 가지는 간선을 갖도록 하면 메모리를 많이 아낄 수 있습니다. 하지만, 그러면 노드 간선 존재 여부를 확인해야 하기 때문에 배열의 index로 접근하는 것보다는 느릴 것입니다. trade off 관계에서 속도에 더 중점을 두었습니다.

  • 다익스트라 알고리즘 처리를 heap이 빌때까지 수행하는 while문으로 바꾸셨는데 왜 바꿨나요?

    • function 재귀 함수는 함수가 호출될 때 마다 Stack 영역에 함수가 쌓이게 됩니다. 즉, while문은 Scope만 새로 생기면 되지만, 재귀 함수로 처리하면 함수가 실행될 때마다 실행 컨텍스트가 생겨야 하고 그만큼 더많은 비용이 생깁니다.

  • 더 빠르게 처리할 수는 없을까요?

    • 글쎄요?? 음 우선 생각나는 건 없네요. 비트연산 하기에는 너무 array가 긴 것들 만 있는 것 같고... ArrayBuffer를 사용하기에도 크기가 가변적인 자료형을 사용하기 때문에 안될 것 같아요. 굳이 찾으면 Worker를 사용하면 되려나요..? 잘 모르겠습니다.

image.webp