Graph Editor
All posts

A* shortest path

GGraph EditorOfficial3 hours ago

Dijkstra with a sense of direction — a heuristic steers the search toward the goal, so it explores a fraction of the graph for the same optimal answer.

Complexity: O((V + E) log V) time · O(V) space — the same worst case as Dijkstra, far better in practice with a good heuristic.

The idea

A* orders its frontier by f(n) = g(n) + h(n): g is the real cost from the start to n, and h estimates the cost from n on to the goal. Dijkstra is just A* with h = 0 — it grows outward equally in every direction. Add a heuristic that points at the goal and the frontier stops wandering: it always expands the node that looks most promising toward the destination.

The heuristic makes or breaks it

h must never overestimate the true remaining cost — an admissible heuristic. When it holds, the first time A* settles the goal that route is optimal, exactly like Dijkstra. On a map the natural choice is straight-line (Euclidean) distance: as the crow flies is never longer than any real path. graph.getPosition(node) gives each node's (x, y) to measure it. If the estimate ever runs high, A* can return a slightly suboptimal path — the price of the speed-up.

Watch for it on the canvas

Run it on the map and watch the visited region reach toward the goal in a narrow tongue instead of flooding outward the way Dijkstra does — same path, far fewer nodes touched. The bigger the graph, the wider that gap.

Try it on this map:

Trail networkSample
24 54
# A* shortest path — Dijkstra steered toward the goal by a straight-line
# heuristic, so it explores far fewer nodes. Node positions come from
# graph.getPosition(); edge weights from graph.getWeight() (1 when unweighted).
import heapq

nodes = list(graph.getNodes())
start, goal = nodes[0], nodes[-1]   # first and last node — change as you like


def heuristic(node):
    # straight-line (Euclidean) distance to the goal — never overestimates the
    # real cost when weights are distances, which is what keeps A* correct
    x1, y1 = graph.getPosition(node)
    x2, y2 = graph.getPosition(goal)
    return ((x1 - x2) ** 2 + (y1 - y2) ** 2) ** 0.5


g = {node: float("inf") for node in nodes}   # best cost found to each node
prev = {node: None for node in nodes}
g[start] = 0
# the heap is ordered by f = g + h (estimated total trip through the node)
heap = [(heuristic(start), start)]
seen = set()

while heap:
    _, node = heapq.heappop(heap)
    if node in seen:
        continue
    seen.add(node)
    if node == goal:
        break
    for neighbour in graph.getNeighbors(node):
        if neighbour in seen:
            continue
        tentative = g[node] + graph.getWeight(node, neighbour)
        if tentative < g[neighbour]:
            g[neighbour] = tentative
            prev[neighbour] = node
            heapq.heappush(heap, (tentative + heuristic(neighbour), neighbour))

path = []
node = goal
while node is not None:
    path.append(node)
    node = prev[node]
path.reverse()

if path and path[0] == start:
    print(f"cost {start} -> {goal}: {g[goal]:.0f}")
    print("path:", " -> ".join(path))
    graph.markPath(path)
else:
    print(f"no route from {start} to {goal}")

Then run the very same code on a much larger map and see how little of it A* even looks at:

Grid worldSample
432 771

Open this post in a project to run it and step through the search.

Comments

Nothing yet — start the conversation.