지도에서 길을 찾을 때, 모든 경로를 다 탐색하지 않고도 최단 경로를 찾을 수 있을까요?

개념 정의

A*는 시작점에서 목표까지의 최단 경로 를 찾는 알고리즘입니다. Dijkstra처럼 최단 거리를 보장하면서도, 휴리스틱(heuristic) 을 활용해 불필요한 탐색을 줄입니다.

핵심 공식은 간단합니다.

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

g(n): 시작점에서 n까지의 실제 비용
h(n): n에서 목표까지의 추정 비용 (휴리스틱)
f(n): 총 예상 비용

f(n)이 가장 작은 노드를 우선 탐색합니다.

왜 필요한가

BFS와 Dijkstra도 최단 경로를 찾지만, 방향성 없이 모든 방향으로 퍼져나갑니다.

PLAINTEXT
BFS/Dijkstra: 시작점에서 동심원처럼 전 방향 탐색
A*: 목표 방향으로 우선 탐색, 필요할 때만 다른 방향 확인

실제로 내비게이션에서 서울→부산 경로를 찾을 때, 서울 북쪽 방향까지 다 탐색하는 건 낭비입니다. A*는 남쪽 방향을 우선 탐색하므로 훨씬 빠릅니다.

BFS vs Dijkstra vs A*

알고리즘가중치휴리스틱최적해 보장시간 복잡도
BFS동일 가중치없음OO(V + E)
Dijkstra양의 가중치없음OO(E log V)
A*양의 가중치있음조건부 OO(E log V) 이하*

A의 실제 성능은 휴리스틱의 품질에 따라 달라집니다.

동작 원리

알고리즘 흐름

  1. 시작 노드를 우선순위 큐(open set)에 넣습니다
  2. f(n)이 가장 작은 노드를 꺼냅니다
  3. 목표 노드면 종료합니다
  4. 이웃 노드들의 g, h, f를 계산하여 큐에 넣습니다
  5. 2~4를 반복합니다
JAVA
public class AStar {

    // 노드 정보
    static class Node implements Comparable<Node> {
        int x, y;
        int g; // 시작→현재 실제 비용
        int f; // g + h (총 예상 비용)

        Node(int x, int y, int g, int f) {
            this.x = x; this.y = y;
            this.g = g; this.f = f;
        }

        @Override
        public int compareTo(Node o) {
            return Integer.compare(this.f, o.f);
        }
    }

    // 4방향 이동
    private static final int[] dx = {0, 0, 1, -1};
    private static final int[] dy = {1, -1, 0, 0};

    public List<int[]> findPath(int[][] grid, int[] start, int[] goal) {
        int rows = grid.length, cols = grid[0].length;
        int[][] gCost = new int[rows][cols];
        int[][] parentX = new int[rows][cols];
        int[][] parentY = new int[rows][cols];

        for (int[] row : gCost) Arrays.fill(row, Integer.MAX_VALUE);

        PriorityQueue<Node> openSet = new PriorityQueue<>();
        boolean[][] closed = new boolean[rows][cols];

        // 시작점 초기화
        gCost[start[0]][start[1]] = 0;
        int h = heuristic(start[0], start[1], goal[0], goal[1]);
        openSet.offer(new Node(start[0], start[1], 0, h));
        parentX[start[0]][start[1]] = -1;
        parentY[start[0]][start[1]] = -1;

        while (!openSet.isEmpty()) {
            Node curr = openSet.poll();

            // 목표 도달
            if (curr.x == goal[0] && curr.y == goal[1]) {
                return reconstructPath(parentX, parentY, goal);
            }

            if (closed[curr.x][curr.y]) continue;
            closed[curr.x][curr.y] = true;

            // 이웃 탐색
            for (int d = 0; d < 4; d++) {
                int nx = curr.x + dx[d];
                int ny = curr.y + dy[d];

                if (nx < 0 || nx >= rows || ny < 0 || ny >= cols) continue;
                if (grid[nx][ny] == 1 || closed[nx][ny]) continue; // 벽이면 스킵

                int newG = curr.g + 1; // 이동 비용 1
                if (newG < gCost[nx][ny]) {
                    gCost[nx][ny] = newG;
                    int newF = newG + heuristic(nx, ny, goal[0], goal[1]);
                    openSet.offer(new Node(nx, ny, newG, newF));
                    parentX[nx][ny] = curr.x;
                    parentY[nx][ny] = curr.y;
                }
            }
        }
        return Collections.emptyList(); // 경로 없음
    }

    // 맨해튼 거리 휴리스틱
    private int heuristic(int x1, int y1, int x2, int y2) {
        return Math.abs(x1 - x2) + Math.abs(y1 - y2);
    }

    // 경로 역추적
    private List<int[]> reconstructPath(int[][] parentX, int[][] parentY, int[] goal) {
        List<int[]> path = new ArrayList<>();
        int x = goal[0], y = goal[1];
        while (x != -1) {
            path.add(new int[]{x, y});
            int px = parentX[x][y];
            int py = parentY[x][y];
            x = px; y = py;
        }
        Collections.reverse(path);
        return path;
    }
}

휴리스틱 함수 설계

휴리스틱의 품질이 A*의 성능을 결정합니다.

허용 가능(admissible) 조건

h(n)이 실제 비용을 절대 초과하지 않아야 최적해를 보장합니다.

PLAINTEXT
h(n) ≤ 실제 비용(n → 목표)    →  최적해 보장 (admissible)
h(n) > 실제 비용              →  최적해 미보장 (하지만 더 빠를 수 있음)

일관성(consistent) 조건

모든 이웃 n'에 대해 h(n) ≤ cost(n, n') + h(n')이면 일관성이 있습니다. 일관성이 있으면 한 번 방문한 노드를 재방문할 필요가 없어져 효율적입니다.

대표적인 휴리스틱

JAVA
// 맨해튼 거리 — 4방향 격자에 적합
int manhattan(int x1, int y1, int x2, int y2) {
    return Math.abs(x1 - x2) + Math.abs(y1 - y2);
}

// 유클리드 거리 — 자유로운 이동에 적합
double euclidean(int x1, int y1, int x2, int y2) {
    return Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2));
}

// 체비셰프 거리 — 8방향 격자에 적합
int chebyshev(int x1, int y1, int x2, int y2) {
    return Math.max(Math.abs(x1 - x2), Math.abs(y1 - y2));
}
이동 방식적합한 휴리스틱
4방향 (상하좌우)맨해튼 거리
8방향 (대각선 포함)체비셰프 거리
자유 이동유클리드 거리

휴리스틱과 성능의 관계

PLAINTEXT
h(n) = 0       → Dijkstra와 동일 (가장 느림, 최적해 보장)
h(n) < 실제비용  → 적당히 빠름, 최적해 보장
h(n) = 실제비용  → 최적 (직선으로 목표까지 탐색)
h(n) > 실제비용  → 매우 빠르지만 최적해 미보장

IDA* (Iterative Deepening A*)

메모리가 부족할 때 사용하는 변형입니다. A는 열린 집합에 많은 노드를 저장하므로 메모리가 문제가 될 수 있습니다. IDA는 깊이 제한을 f값 기준으로 점진적으로 늘려가며 DFS를 반복합니다.

JAVA
// IDA* 개념적 구현
public int idaStar(int[] start, int[] goal) {
    int threshold = heuristic(start[0], start[1], goal[0], goal[1]);

    while (true) {
        int result = dfsSearch(start, 0, threshold, goal);
        if (result == -1) return threshold; // 목표 도달
        if (result == Integer.MAX_VALUE) return -1; // 경로 없음
        threshold = result; // 다음 임계값
    }
}

백엔드/실무 연결

게임 길찾기

A는 게임에서 가장 많이 사용되는 길찾기 알고리즘입니다. NPC의 이동 경로, 전략 게임의 유닛 이동 등에 핵심적으로 쓰입니다. Unity, Unreal Engine 모두 A 기반 네비게이션 시스템을 내장하고 있습니다.

내비게이션 시스템

실제 내비게이션에서는 A*의 변형들이 쓰입니다.

  • *ALT (A with Landmarks and Triangle inequality)**: 랜드마크를 미리 정하고 전처리
  • Contraction Hierarchies: 도로 네트워크의 계층 구조를 이용한 전처리

로봇 경로 계획

로봇의 움직임 계획(motion planning)에서 장애물을 피해 목적지까지의 경로를 찾을 때 A* 또는 그 변형(D*, Theta*)을 사용합니다.

API 게이트웨이 라우팅

요청을 여러 마이크로서비스를 거쳐 처리할 때, 지연 시간을 최소화하는 경로를 찾는 것도 A*와 유사한 원리입니다. 각 서비스의 예상 응답 시간이 휴리스틱이 됩니다.

주의할 점

허용 불가능(inadmissible) 휴리스틱을 사용하여 최적해를 놓치는 실수

h(n)이 실제 비용보다 크면(과대평가), A*가 최단 경로가 아닌 경로를 반환할 수 있습니다. 최적해가 필요하면 반드시 허용 가능(admissible) 휴리스틱을 사용해야 합니다.

4방향 이동에서 유클리드 거리를 휴리스틱으로 사용하는 실수

4방향 이동만 가능한 격자에서 유클리드 거리는 실제 이동 거리보다 작아 비효율적입니다. 맨해튼 거리(|dx| + |dy|)가 정확한 하한이므로 더 적절합니다.


정리

  • A*는 f(n) = g(n) + h(n) 공식으로 최단 경로를 효율적으로 찾는 알고리즘입니다
  • g(n) 은 실제 비용, h(n) 은 추정 비용(휴리스틱)입니다
  • 휴리스틱이 admissible(과소평가)하면 최적해를 보장합니다
  • h(n) = 0이면 Dijkstra, h(n)이 정확할수록 탐색이 빨라집니다
  • 격자 유형에 따라 맨해튼, 유클리드, 체비셰프 거리를 선택합니다
  • 게임 길찾기, 내비게이션, 로봇 경로 계획 등 광범위하게 활용됩니다
댓글 로딩 중...