본문 바로가기
코딩테스트 준비/자료구조 & 알고리즘

최단 경로 알고리즘 총 정리(다익스트라,벨만-포드 알고리즘,A*알고리즘)

by 코딩하는 돼징 2025. 4. 17.
반응형

최단 경로 알고리즘이란?

어떻게 하면 가정 적은 비용으로 목적지에 도착할 수 있을까?를 생각하는 알고리즘이다.


1. 다익스트라 알고리즘(Dijkstra Algorithm)

하나의 시작 정점에서 다른 모든 정점까지의 최단 경로를 찾는 알고리즘이다. 단 모든 간선의 가중치는 0보다 크거나 같아야 한다.

이 알고리즘의 핵심은 지금 까지 찾은 가장 짧은 길을 기억하고 그걸 이용해서 다음 길을 더 쉽게 계산하는 것이다.  즉 지금까지 찾은 가장 짧은 길을 계속 저장해두고 DP처럼 기존에 구한 결과를 재활용한다.

 

01 작동 과정

distance[] ← ∞, distance[start] ← 0
visited[] ← false

반복:
    현재 노드 ← 방문 안 한 노드 중 최소 distance
    인접 노드의 거리 갱신
    현재 노드 visited = true
종료

1. 출발 노드를 설정

2. 출발 노드를 기준으로 각 노드의 거리를 초기화 한다.

3. 아직 방문하지 않는 노드 중에서 가장 거리가 짧은 노드를 선택한다.

4. 해당 노드를  거쳐 갈 수 있는 인접 노드들의 거리 값을 확인하고 기존 거리보다 짧으면 갱신한다.

5. 3~4 과정을 모든 노드를 방문할 때까지 반복한다.


02 예제

 

출처 : https://en.wikipedia.org/wiki/Dijkstra%27s_algorithm

1) 초기 상태

distance = [0, ∞, ∞, ∞, ∞, ∞]

2) 현재노드

1 → 2 (7), 1 → 3 (9), 1 → 6 (14) 갱신

distance = [0, 7, 9, ∞, ∞, 14]

3)현재 노드 2

2 → 4 (7 + 15 = 22)

distance = [0, 7, 9, 22, ∞, 14]

4) 현재 노드 3

 

3 → 4 (9 + 11 = 20 → 더 짧음), 3 → 6 (9 + 2 = 11 → 더 짧음)

 

distance = [0, 7, 9, 20, ∞, 11]

5) 현재 노드 6

6 → 5 (11 + 9 = 20)

distance = [0, 7, 9, 20, 20, 11]

6) 현재 노드 4

4 → 5 (20 + 6 = 26 기존보다 큼 → 변경 없음)

distance = [0, 7, 9, 20, 20, 11]

7) 현재 노드 5

더이상 갱신 없음

distance = [0, 7, 9, 20, 20, 11]

03 작동 과정에 따른 C# 코드

1. 출발 노드를 설정

static void Dijkstra(int[,] graph, int start)

2. 출발 노드를 기준으로 각 노드의 거리를 초기화 한다.

int[] distance = new int[n];
for (int i = 0; i < n; i++)
    distance[i] = int.MaxValue;

distance[start] = 0;

3. 아직 방문하지 않는 노드 중에서 가장 거리가 짧은 노드를 선택한다.

int min = int.MaxValue;
int minIndex = -1; // 현재 가장 짧은 거리값은 가진 노드

// 방문하지 않은 노드 중에서 최단 거리인 노드(minIndex)를 찾는 루프
for (int j = 0; j < n; j++)
{
    if (!visited[j] && distance[j] <= min)
    {
        min = distance[j];
        minIndex = j; 
    }
}

4. 해당 노드를  거쳐 갈 수 있는 인접 노드들의 거리 값을 확인하고 기존 거리보다 짧으면 갱신한다.

for (int v = 0; v < n; v++)
{
    // 현재 노드 u에서 인접 노드 v로 갈 수 있을 때 그 경로가 더 짧다면 distance[v]를 갱신
    if (!visited[v] && graph[u, v] != 0 && distance[u] != int.MaxValue &&
        distance[u] + graph[u, v] < distance[v])
    {
        distance[v] = distance[u] + graph[u, v];
    }
}

 

5. 3~4 과정을 모든 노드를 방문할 때까지 반복한다.


04 C# 코드 전문

// 1. 출발 노드 설정
static void Dijkstra(int[,] graph, int start)
{
    int n = graph.GetLength(0);

    // 2. 거리 초기화
    bool[] visited = new bool[n];
    int[] distance = new int[n];
    for (int i = 0; i < n; i++) distance[i] = int.MaxValue;
    distance[start] = 0;

    for (int i = 0; i < n - 1; i++)
    {
        // 3. 최소 거리 노드 선택
        int min = int.MaxValue;
        int minIndex = -1;
        for (int j = 0; j < n; j++)
        {
            if (!visited[j] && distance[j] <= min)
            {
                min = distance[j];
                minIndex = j;
            }
        }

        int u = minIndex;
        visited[u] = true;

        // 4. 인접 노드 거리 갱신
        for (int v = 0; v < n; v++)
        {
            if (!visited[v] && graph[u, v] != 0 &&
                distance[u] != int.MaxValue &&
                distance[u] + graph[u, v] < distance[v])
            {
                distance[v] = distance[u] + graph[u, v];
            }
        }

        // 5. 모든 노드를 방문할 때까지 반복
    }
}

 


벨만-포드 알고리즘(Bellman-Ford Algorithm)

음의 가중치를 갖는 간선이 있는 그래프에서도 사용할 수 있는 최단 경로 알고리즘입니다. 음수 사이클이 있는 경우에도 탐지할 수 있습니다.

 

음수 사이클이란?

사이클(한 반퀴 도는 경로)을 돌고 나서 총 비용이 음수가 되는 경우

 

01 작동 과정

distance[] ← ∞, distance[start] ← 0

반복 (V - 1회):
    모든 간선(u → v) 검사:
        distance[v] = min(distance[v], distance[u] + cost)

마지막 1회 더 검사:
    갱신 발생 시 → 음수 사이클 존재

1. 출발 노드를 설정

2. 시작 노드를 제외한 모든 노드의 거리를 무한대로 초기화한다.

3. 모든 간선을 N-1번 반복하며 검사한다.(N은 노드의 개수)

왜 N-1개인가요? 어떤 경로든 최대 N-1개의 간선만 거쳐서 도달할 수 있다.

예를 들어 노드가 5개이면 가장 멀리 돌아가도 최대 4개의 간선만지나면 도착이 가능하다. 

4.  AB로가는 간선이 있고, distance[A] + cost < distance[B]이면 갱신한다.

5. 마지막으로 한 번 더 모든 간선을 검사해서 갱신한다. 갱신이 일어나면 음수 사이클이 존재한다는 뜻이다.


02 예제

출처 : https://en.wikipedia.org/wiki/Bellman%E2%80%93Ford_algorithm

실행 이해가 안가서 엑셀에 다 적어보았다 하핳..

1) 초기 상태

distance = [0, ∞, ∞, ∞, ∞]

 

2) 1번째 반복 (간선 전체 확인)

s → t :  0 + 6 < ∞ distance[1] = 6

s → y : 0 + 7 < ∞ distance[3] = 7

distance = [0, 6, 4, 7, 2]

 

3) 2번째 반복

t → x : 6 + 5 = 11 

t → z : 6 + (-4) = 2 
y→ x : 7 + (-3) = 4 

distance = [0, 2, 4, 7, 2]

 

4) 3번째 반복

x→ t : 4 + (-2) = 2 

distance = [0, 2, 4, 7, -2]

5) 4 번째 반복

distance = [0, 2, 4, 7, -2]

03 작동 과정에 따른 C# 코드

1. 출발 노드를 설정

2. 시작 노드를 제외한 모든 노드의 거리를 무한대로 초기화한다.

int[] distance = new int[nodeCount];
for (int i = 0; i < nodeCount; i++)
    distance[i] = int.MaxValue;

distance[start] = 0; // 출발 노드는 거리 0

3. 모든 간선을 N-1번 반복하며 검사한다.(N은 노드의 개수)

for (int i = 0; i < n - 1; i++) // N-1번 반복
{
    for (int u = 0; u < n; u++) // u = 출발노드
    {
        for (int v = 0; v < n; v++) // v = 도착노드
        {
            // 4번 로직이 여기로
        }
    }
}

왜 N-1개인가요? 어떤 경로든 최대 N-1개의 간선만 거쳐서 도달할 수 있다.

예를 들어 노드가 5개이면 가장 멀리 돌아가도 최대 4개의 간선만지나면 도착이 가능하다. 

4.  A → B로가는 간선이 있고, distance[A] + cost < distance[B]이면 갱신한다.

if (graph[u, v] != int.MaxValue && distance[u] != int.MaxValue &&
    distance[u] + graph[u, v] < distance[v])
{
    distance[v] = distance[u] + graph[u, v];
}

조건문 설명

graph[u, v] != int.MaxValue - 간선이 실제로 존재하는지 체크

distance[u] != int.MaxValue - 시작노드와 이어져있는지 체크

distance[u] + graph[u, v] < distance[v] - u를 거쳐서 v로 가는게 전에 구했던 경로보다 짧은지

5. 마지막으로 한 번 더 모든 간선을 검사해서 음수 사이클이 존재하는지 확인

bool hasNegativeCycle = false;

for (int u = 0; u < n; u++)
{
    for (int v = 0; v < n; v++)
    {
        if (graph[u, v] != int.MaxValue && distance[u] != int.MaxValue &&
            distance[u] + graph[u, v] < distance[v])
        {
            hasNegativeCycle = true;
            break;
        }
    }
}

05 코드 전문

static void BellmanFord(int[,] graph, int start)
{
    int n = graph.GetLength(0);
    int[] distance = new int[n];

    // 2. 거리 초기화
    for (int i = 0; i < n; i++)
        distance[i] = int.MaxValue;

    distance[start] = 0;

    // 3. 모든 간선을 N-1번 반복
    for (int i = 0; i < n - 1; i++)
    {
        for (int u = 0; u < n; u++)
        {
            for (int v = 0; v < n; v++)
            {
                // 4. 값 갱신
                if (graph[u, v] != int.MaxValue && distance[u] != int.MaxValue &&
                    distance[u] + graph[u, v] < distance[v])
                {
                    distance[v] = distance[u] + graph[u, v];
                }
            }
        }
    }

    // 5. 음수 사이클 확인
    bool hasNegativeCycle = false;
    for (int u = 0; u < n; u++)
    {
        for (int v = 0; v < n; v++)
        {
            if (graph[u, v] != int.MaxValue && distance[u] != int.MaxValue &&
                distance[u] + graph[u, v] < distance[v])
            {
                hasNegativeCycle = true;
                break;
            }
        }
    }

}

 

다익스트라 vs 벨만-포드

다익스트라

특징 : 방문한 정점 중에서 가장 가까운 노드부터 순서대로 확장

예를 들어 배달 경로처럼 차례차례 가까운 집부터 배달

시간복잡도 : 우선순위큐를 사용하는 경우 O((V + E) log V), 사용하지 않는 경우  O(V^2)

벨만-포드

특징 : 모든 간선을 무작위로 반복해서 살펴보고 더 짧은 길을 선택

예를 들어 계획없이 길을 걷다가 더 짧은 경로를 찾으면 언제든 새로운 경로로 가기

시간 복잡도 : O(V × E)


A* 알고리즘 (A-star Algorithm)

휴리스틱 함수를 사용하여 각 정점까지의 예상 비용을 계산하고, 가장 낮은 예상 비용을 가진 정점을 선택하여 탐색합니다.

 

01 작동 과정

OPEN ← 시작 노드
CLOSED ← 비어 있음

반복:
    current ← f(n) 가장 낮은 노드
    목표이면 종료

    이웃 노드 확인:
        갈 수 없는 곳/이미 본 곳 무시
        더 짧은 경로면 g, f 갱신 + parent 설정
        처음 본 노드면 OPEN에 추가

    current → CLOSED에 이동

1. 시작 노드를 OPEN리스트에 넣는다.

OPEN = 탐색 예정인 후보 노드들

CLOSED = 탐색이 끝난 노드들

2. OPEN에서 f(n)이 가장 낮은 노드를 꺼낸다.

g(n) = 시작점에서 현재까지 온 실제거리

h(n) = 현재에서 목표까지의 예상 거리

f(n) = g(n) + h(n)

이 노드를 현재노드라 칭하고 OPEN에서 제거한 다음 CLOED에 넣는다.

3. 목표 지점인지 확인

current 노드가 목표지점이면 경로를 복원해서 반환하고 끝

4. current노드의 이웃노드들 확인

1) 벽이거나 이미 CLOSED에 있는 노드면 건너뛴다.

2) 현재 노드를 거쳐서 가는 경로가 더 짧거나 아직 OPEN에 등록된 적이 없다면

 

g(n) 갱신: 더 짧은 거리로 도달했다면 거리 갱신

h(n) 계산: 휴리스틱(목표까지 예상 거리) 계산

f(n) 계산: g + h

parent 설정: 어디서 왔는지 기록 (경로 복원용)

OPEN에 등록: 처음 보는 노드였다면

5. OPEN이 빌때까지 반복

OPEN이 완전히 비었는데 도달 못했다면 경로없음

 


02 예시

출처:https://en.wikipedia.org/wiki/A*_search_algorithm

 

0단계 : 초록색(시작)점에서 근처 노드 탐색

 

Start → a (거리 1.5) → g(a) = 1.5, f(a) = 1.5 + 4 = 5.5

Start → d (거리 2.0) → g(d) = 2.0, f(d) = 2.0 + 4.5 = 6.5

 OPEN = {a(5.5), d(6.5)}

 

f가 가장 낮은 a선택

1단계 : a 선택(f=5.5)

a → b (거리 2.0)

g(b) = g(a) + 2 = 1.5 + 2 = 3.5

h(b) = 2

f(b) = 3.5 + 2 = 5.5

OPEN = {d (6.5), b (5.5)}

f가 가장 낮은 b선택

2단계 b선택(f=5.5)

 

b → c (3.0)

g(c) = 3.5 + 3 = 6.5

h(c) = 4

f(c) = 6.5 + 4 = 10.5

OPEN = {d (6.5), c (10.5)}

 

f가 가장 낮은 d 선택

3단계 d선택(f=6.5)

d → e (거리 3.0)
g(e) = 2.0 + 3.0 = 5.0, h(e) = 2
f(e) = 7.0

OPEN = {c(10.5), e(7.0)}

e → Goal (거리 2.0)
g(Goal) = 5.0 + 2.0 = 7.0, h(Goal) = 0
f(Goal) = 7.0

그래서 최종 경로

Start → d → e → Goal

03 작동 과정에 따른 C# 코드

 

public class Node
{
    public int X, Y;
    public int G; // 지금까지의 거리
    public int H; // 목적지까지의 예상 거리
    public int F => G + H; // 총 비용
    public Node Parent;

    public Node(int x, int y)
    {
        X = x;
        Y = y;
    }

    public override bool Equals(object obj)
    {
        if (obj is Node other)
        {
            return this.X == other.X && this.Y == other.Y;
        }
        return false;
    }

    public override int GetHashCode()
    {
        return (X, Y).GetHashCode();
    }
}

1. 시작 노드를 OPEN리스트에 넣는다.

OPEN = 탐색 예정인 후보 노드들

CLOSED = 탐색이 끝난 노드들

var openSet = new List<Node>();               // OPEN = 탐색 예정 노드들
var closedSet = new HashSet<Node>();          // CLOSED = 탐색이 끝난 노드들

start.G = 0;
start.H = Heuristic(start, goal);
openSet.Add(start);                           // 시작 노드를 OPEN에 등록

2. OPEN에서 f(n)이 가장 낮은 노드를 꺼낸다.

g(n) = 시작점에서 현재까지 온 실제거리

h(n) = 현재에서 목표까지의 예상 거리

f(n) = g(n) + h(n)

이 노드를 현재노드라 칭하고 OPEN에서 제거한 다음 CLOED에 넣는다.

Node current = openSet[0];
foreach (var node in openSet)
    if (node.F < current.F)
        current = node;

openSet.Remove(current);
closedSet.Add(current); // 탐색 완료 → CLOSED에 넣기

3. 목표 지점인지 확인

current 노드가 목표지점이면 경로를 복원해서 반환하고 끝

if (current.X == goal.X && current.Y == goal.Y)
{
    // 경로 복원
    var path = new List<Node>();
    while (current != null)
    {
        path.Add(current);
        current = current.Parent;
    }
    path.Reverse();
    return path;
}

4. current노드의 이웃노드들 확인

1) 벽이거나 이미 CLOSED에 있는 노드면 건너뛴다.

2) 현재 노드를 거쳐서 가는 경로가 더 짧거나 아직 OPEN에 등록된 적이 없다면

g(n) 갱신: 더 짧은 거리로 도달했다면 거리 갱신

h(n) 계산: 휴리스틱(목표까지 예상 거리) 계산

f(n) 계산: g + h

foreach (var neighbor in GetNeighbors(current, grid))
{
    // 1) 벽이거나 CLOSED에 있으면 skip
    if (closedSet.Contains(neighbor)) continue;

    int tentativeG = current.G + 1; // 인접 노드까지 거리 (기본적으로 1로 가정)

    // 2) 더 짧은 경로거나, 처음 보는 노드라면
    bool inOpen = openSet.Contains(neighbor);
    if (!inOpen || tentativeG < neighbor.G)
    {
        neighbor.G = tentativeG;
        neighbor.H = Heuristic(neighbor, goal);
        neighbor.Parent = current;

        if (!inOpen)
            openSet.Add(neighbor); // 처음 보는 노드라면 등록
    }
}

parent 설정: 어디서 왔는지 기록 (경로 복원용)

OPEN에 등록: 처음 보는 노드였다면

5. OPEN이 빌때까지 반복

OPEN이 완전히 비었는데 도달 못했다면 경로없음

while (openSet.Count > 0)
{
    // 위 과정 반복
    ...
}

return null; // OPEN이 비었는데 목표 도달 못했으면 → 경로 없음

04 코드 전문

using System;
using System.Collections.Generic;

public class AStar
{
    public class Node
    {
        public int X, Y;
        public int G; // 지금까지의 거리
        public int H; // 휴리스틱 (예상 거리)
        public int F => G + H; // 총 비용
        public Node Parent;

        public Node(int x, int y)
        {
            X = x;
            Y = y;
        }

        public override bool Equals(object obj)
        {
            if (obj is Node other)
            {
                return this.X == other.X && this.Y == other.Y;
            }
            return false;
        }

        public override int GetHashCode()
        {
            return (X, Y).GetHashCode();
        }
    }

    static int Heuristic(Node a, Node b)
    {
        // 맨해튼 거리
        return Math.Abs(a.X - b.X) + Math.Abs(a.Y - b.Y);
    }

    static List<Node> GetNeighbors(Node node, int[,] grid)
    {
        var neighbors = new List<Node>();
        int[] dx = { 0, 1, 0, -1 };
        int[] dy = { -1, 0, 1, 0 };

        for (int i = 0; i < 4; i++)
        {
            int nx = node.X + dx[i];
            int ny = node.Y + dy[i];

            if (nx >= 0 && ny >= 0 && nx < grid.GetLength(0) && ny < grid.GetLength(1))
            {
                if (grid[nx, ny] == 0) // 0은 이동 가능, 1은 벽
                    neighbors.Add(new Node(nx, ny));
            }
        }

        return neighbors;
    }

    public static List<Node> FindPath(int[,] grid, Node start, Node goal)
    {
        var openSet = new List<Node>();
        var closedSet = new HashSet<Node>();
        start.G = 0;
        start.H = Heuristic(start, goal);
        openSet.Add(start);

        while (openSet.Count > 0)
        {
            // f 값이 가장 작은 노드 선택
            Node current = openSet[0];
            foreach (var node in openSet)
                if (node.F < current.F) current = node;

            if (current.X == goal.X && current.Y == goal.Y)
            {
                // 경로 추적
                var path = new List<Node>();
                while (current != null)
                {
                    path.Add(current);
                    current = current.Parent;
                }
                path.Reverse();
                return path;
            }

            openSet.Remove(current);
            closedSet.Add(current);

            foreach (var neighbor in GetNeighbors(current, grid))
            {
                if (closedSet.Contains(neighbor)) continue;

                int tentativeG = current.G + 1;
                bool inOpen = openSet.Contains(neighbor);

                if (!inOpen || tentativeG < neighbor.G)
                {
                    neighbor.G = tentativeG;
                    neighbor.H = Heuristic(neighbor, goal);
                    neighbor.Parent = current;

                    if (!inOpen)
                        openSet.Add(neighbor);
                }
            }
        }

        return null; // 경로 없음
    }
}

 

 

 

 

 

 

 

 

 

 

반응형

댓글