cv.cm

코딩 · 알고리즘

A* 탐색 알고리즘 쉽게 이해하기: 동작 원리, 코드, 시간 복잡도

7분 분량

핵심 답변

A* 탐색은 지금까지의 비용 g(n)과 남은 비용의 추정값 h(n)을 합쳐, 시작 노드에서 하나의 목표 노드까지의 최단 경로를 찾습니다. 항상 f(n) = g(n) + h(n)이 가장 작은 노드를 확장합니다. 휴리스틱이 실제 남은 비용을 과대평가하지 않으면 A*는 최적 경로를 보장하며, 좋은 휴리스틱을 쓰면 다익스트라 알고리즘보다 훨씬 적은 노드를 탐색합니다.

A*가 해결하는 문제

다익스트라 알고리즘은 목표에서 멀어지는 방향까지 포함해 모든 방향으로 거리가 커지는 동심원 순서로 노드를 확장합니다. 목표 위치를 알고 있다면 이는 낭비입니다. A*는 남은 거리의 추정값인 휴리스틱을 더해 탐색이 목표 쪽으로 끌려가게 합니다. 게임, 로보틱스, 지도에서 길 찾기의 표준 알고리즘입니다.

A*는 1968년 피터 하트(Peter Hart), 닐스 닐손(Nils Nilsson), 버트럼 라파엘(Bertram Raphael)이 "A Formal Basis for the Heuristic Determination of Minimum Cost Paths"에서 소개했습니다.

핵심 아이디어

기호의미
g(n)시작점에서 n까지 지금까지 알려진 최선 경로의 비용.
h(n)n에서 목표까지 비용의 휴리스틱 추정값.
f(n) = g(n) + h(n)n을 지나는 가장 저렴한 전체 경로의 추정 비용. 오픈 집합은 f 순으로 정렬됩니다.
  1. 시작 노드를 g = 0, f = h(start)로 우선순위 큐(오픈 집합)에 넣습니다.
  2. f가 가장 작은 노드를 꺼냅니다. 그것이 목표라면 경로를 복원하고 종료합니다.
  3. 각 이웃에 대해 임시 g = g(current) + 간선 비용을 계산합니다. 이웃의 기존 g보다 좋다면 값을 기록하고 부모를 설정한 뒤 f = g + h로 이웃을 큐에 넣습니다.
  4. 오픈 집합이 비면 경로가 없습니다.

모든 노드에서 h(n) = 0이면 A*는 정확히 다익스트라 알고리즘이 됩니다. 완벽한 휴리스틱이면 최적 경로를 따라 곧장 나아갑니다.

휴리스틱 고르기

휴리스틱이 실제 남은 비용을 과대평가하지 않으면 허용 가능(admissible)하다고 합니다. 모든 간선 n → m에 대해 h(n) <= cost(n, m) + h(m)이면 일관적(consistent, 또는 단조적)이라고 합니다. 일관적인 휴리스틱은 허용 가능하기도 하며, 이 경우 각 노드를 한 번만 확장하면 됩니다.

이동 방식휴리스틱공식
격자에서 4방향, 비용 1맨해튼 거리abs(dx) + abs(dy)
8방향, 대각선 비용 1체비쇼프 거리max(abs(dx), abs(dy))
8방향, 대각선 비용 √2옥타일 거리max(dx, dy) + (sqrt(2) - 1) * min(dx, dy) (dx, dy는 절댓값)
임의 방향, 직선 이동유클리드 거리sqrt(dx * dx + dy * dy)

격자에서의 파이썬 구현

아래 격자는 5행 7열이며 #은 5열의 벽입니다. 칸은 (row, column)으로 나타냅니다. 상하좌우로 비용 1씩 이동하므로 맨해튼 거리는 허용 가능하고 일관적입니다.

import heapq
def a_star(grid, start, goal):
    rows, cols = len(grid), len(grid[0])
    h = lambda p: abs(p[0] - goal[0]) + abs(p[1] - goal[1])
    g = {start: 0}
    parent = {start: None}
    open_heap = [(h(start), start)]
    while open_heap:
        f, cur = heapq.heappop(open_heap)
        if cur == goal:
            path = []
            while cur is not None:
                path.append(cur)
                cur = parent[cur]
            return path[::-1]
        if f > g[cur] + h(cur):
            continue                      # outdated entry
        r, c = cur
        for nb in ((r + 1, c), (r - 1, c), (r, c + 1), (r, c - 1)):
            if 0 <= nb[0] < rows and 0 <= nb[1] < cols and grid[nb[0]][nb[1]] != "#":
                ng = g[cur] + 1
                if ng < g.get(nb, float("inf")):
                    g[nb] = ng
                    parent[nb] = cur
                    heapq.heappush(open_heap, (ng + h(nb), nb))
    return None
grid = [".......",
        ".....#.",
        ".....#.",
        ".....#.",
        "......."]
path = a_star(grid, (2, 3), (2, 6))
print(len(path) - 1, path)
# 7 [(2, 3), (2, 4), (1, 4), (0, 4), (0, 5), (0, 6), (1, 6), (2, 6)]

단계별 예제

위 격자에서 시작점 (2, 3)은 목표 (2, 6)까지 직선으로 3칸 떨어져 있으므로 첫 f는 0 + 3 = 3입니다. 벽이 그 직선을 막고 있어 실제 최단 경로는 7단계입니다. A*는 이를 단계적으로 알아냅니다. f가 가장 작은 칸들을 모두 확장하고, 그것이 바닥나면 그다음으로 작은 f를 확장합니다.

f 값확장된 칸 (순서대로)일어난 일
3(2, 3), (2, 4)벽에 닿을 때까지 목표를 향해 직진.
5(1, 3), (1, 4), (2, 2), (3, 3), (3, 4)옆이나 뒤로 한 걸음 가면 비용이 2 늘어납니다. 여전히 막혀 있음.
7(0, 3), (0, 4), (0, 5), (0, 6), (1, 2), (1, 6), (2, 1), (2, 6)벽의 윗쪽으로 우회. 목표가 f = 7로 꺼내짐.

A*는 열린 칸 32개 중 15개를 확장했습니다. 다익스트라 알고리즘(h = 0인 A*)은 모든 열린 칸이 시작점에서 최대 7걸음 거리이므로 32개를 모두 확장합니다. 넓게 트인 큰 지도에서는 절감 효과가 훨씬 큽니다.

시간 복잡도와 공간 복잡도

항목비용이유
최악의 시간O((V + E) log V)다익스트라와 같습니다. h = 0 같은 쓸모없는 휴리스틱은 아무 안내도 하지 못합니다.
일반적인 시간다익스트라보다 훨씬 적음전적으로 휴리스틱의 품질에 달려 있습니다. 완벽한 h라면 경로 위의 노드만 확장합니다.
공간O(V)오픈 힙, g 값, 부모 정보가 필요합니다. 아주 큰 지도에서는 메모리가 실제 한계인 경우가 많습니다.

메모리를 아끼는 변형으로 IDA*(반복 깊이 증가 A*)가 있습니다. 현재 경로만 유지하는 대신 노드를 다시 방문합니다.

A*를 쓰는 경우와 실제 활용

  • 목표가 하나이고, 지도의 기하학적 거리처럼 저렴하고 허용 가능한 남은 비용 추정값이 있을 때 A*를 사용합니다.
  • 많은 노드 또는 모든 노드까지의 거리가 필요하거나 쓸 만한 휴리스틱이 없다면 다익스트라를 사용하세요. 휴리스틱의 이점이 적은 가중치 없는 그래프에는 BFS를 사용합니다.
  • 비디오 게임: 타일 맵과 내비게이션 메시에서의 유닛 이동.
  • 로보틱스와 경로 계획: 미리 계산한 랜드마크나 계층 구조와 함께 쓰는 경우가 많습니다.
  • 퍼즐 풀이: 타일의 맨해튼 거리를 휴리스틱으로 쓰는 8퍼즐과 15퍼즐.

자주 묻는 질문

A*는 항상 최적인가요?

휴리스틱이 허용 가능, 즉 남은 비용을 과대평가하지 않으면 A*는 최적 경로를 반환합니다. 과대평가하는 휴리스틱이면 더 긴 경로를 반환할 수 있습니다.

A*와 다익스트라 알고리즘의 차이는 무엇인가요?

다익스트라는 지금까지의 비용 g(n) 순으로 노드를 정렬합니다. A*는 g(n) + h(n) 순으로 정렬하며, h는 목표까지 남은 비용의 추정값입니다. h = 0인 A*가 곧 다익스트라입니다.

격자에서는 어떤 휴리스틱을 써야 하나요?

4방향 이동에는 맨해튼 거리를, 8방향 이동에는 대각선 비용에 따라 체비쇼프 거리나 옥타일 거리를, 자유로운 이동에는 유클리드 거리를 사용하세요.

탐욕적 최선 우선 탐색이란 무엇인가요?

지금까지의 비용은 무시하고 h(n)만으로 노드를 정렬합니다. 대체로 빠르지만 최단 경로를 보장하지는 않습니다.

코딩 튜토리얼 전체