{"id":36163,"date":"2022-10-30T18:33:55","date_gmt":"2022-10-30T18:33:55","guid":{"rendered":"https:\/\/www.askpython.com\/?p=36163"},"modified":"2026-04-30T00:09:36","modified_gmt":"2026-04-30T00:09:36","slug":"a-star-algorithm","status":"publish","type":"post","link":"https:\/\/www.askpython.com\/python\/examples\/a-star-algorithm","title":{"rendered":"A* Algorithm &#8211; Introduction to The Algorithm (With Python Implementation)"},"content":{"rendered":"\n<p>In this article, let me walk you through the A* Algorithm &#8211; one of the most widely used pathfinding algorithms in computer science. I first encountered it while building a game AI, and I keep coming back to it because it solves such a broad range of problems so cleanly. At its core, A* finds the shortest path from a start node to a goal node, making it indispensable for game development, robotics, and GPS navigation.<\/p>\n\n\n\n<p>What makes A* stand out is its use of a heuristic &#8211; a mental shortcut that estimates how far each node is from the goal. This lets it prioritize promising paths without exhaustively exploring every possibility. If you&#8217;ve used Dijkstra&#8217;s algorithm before, A* is like Dijkstra with a GPS &#8211; it still guarantees the optimal path but gets there faster by cheating intelligently.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">TLDR<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li>A* uses f(N) = g(N) + h(N) to evaluate nodes &#8211; g is the actual cost from start, h is the estimated cost to the goal<\/li>\n\n\n\n<li>h must be admissible (never overestimate) for A* to guarantee the optimal path<\/li>\n\n\n\n<li>The algorithm maintains an open set (candidates) and closed set (explored nodes), expanding the lowest f-score node each iteration<\/li>\n\n\n\n<li>Common heuristics: Manhattan distance for grids, Euclidean distance for continuous spaces<\/li>\n\n\n\n<li>Applications: game pathfinding (NPCs, RTS units), GPS routing, robot navigation, puzzle solving (8-puzzle, 15-puzzle)<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">What is the A* Algorithm?<\/h2>\n\n\n\n<p>A* (pronounced &#8220;A-star&#8221;) was first published in 1968 by Peter Hart, Nils Nilsson, and Bertram Raphael at Stanford. It extends the ideas of Dijkstra&#8217;s shortest path algorithm by adding a heuristic that guides the search toward the goal. The result is an algorithm that is both complete (will find a path if one exists) and optimal (will find the shortest path) when the heuristic is admissible.<\/p>\n\n\n\n<p>The algorithm evaluates each node using a cost function:<\/p>\n\n\n\n<p><strong>f(N) = g(N) + h(N)<\/strong><\/p>\n\n\n\n<p>Here, <strong>g(N)<\/strong> is the actual cost of the path from the start node to N &#8211; this is the sum of all edge costs along the path discovered so far. The <strong>h(N)<\/strong> is a heuristic estimate of the minimum cost from N to the goal. This is where domain-specific knowledge enters the algorithm.<\/p>\n\n\n\n<p>A* expands nodes in order of increasing f(N). The open set is a priority queue sorted by f-score. Each iteration, it removes the node with the lowest f-score, adds its neighbors to the open set if they haven&#8217;t been visited or found via a cheaper path, and continues until the goal is removed from the open set.<\/p>\n\n\n\n<p>The algorithm maintains two sets: an <strong>open set<\/strong> of candidate nodes and a <strong>closed set<\/strong> of already-explored nodes. For each neighbor of a node being expanded, A* checks whether a shorter path has been found. If so, it updates that neighbor&#8217;s g-score and parent pointer.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">A* vs Other Search Algorithms<\/h2>\n\n\n\n<p>Understanding what makes A* different requires comparing it to the algorithms it builds on.<\/p>\n\n\n\n<p><strong>Dijkstra&#8217;s Algorithm<\/strong> explores nodes in order of increasing g(N) &#8211; it finds the shortest path but considers all directions equally. This is thorough but slow for large graphs.<\/p>\n\n\n\n<p><strong>Greedy Best-First Search<\/strong> explores nodes in order of increasing h(N) &#8211; it races toward the goal but ignores path cost, so it can find long, winding routes that technically reach the goal faster.<\/p>\n\n\n\n<p>A* sits between them. By combining g(N) and h(N), it balances exploration cost against proximity to the goal. When h(N) = 0 for all nodes, A* becomes Dijkstra. When g(N) = 0 for all nodes, A* becomes Greedy Best-First. The art of using A* is choosing a heuristic that guides the search without overestimating.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Implementing A* in Python<\/h2>\n\n\n\n<p>Let me show you a clean, readable A* implementation. I&#8217;ll use a grid-based example because it&#8217;s intuitive to visualize, but the same structure works for any graph.<\/p>\n\n\n\n<p>A* pathfinding on a 5&#215;5 grid with obstacles. The agent moves up, down, left, or right (no diagonals). Movement cost is 1 per step. The heuristic is Manhattan distance &#8211; the minimum number of steps required ignoring obstacles.<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: python; title: ; notranslate\" title=\"\">\nimport heapq\n\nclass Node:\n    def __init__(self, pos, parent=None):\n        self.pos = pos\n        self.parent = parent\n        self.g = 0  # cost from start\n        self.h = 0  # heuristic cost to goal\n        self.f = 0  # total: g + h\n\n    def __lt__(self, other):\n        return self.f &lt; other.f\n\ndef manhattan(a, b):\n    return abs(a&#x5B;0] - b&#x5B;0]) + abs(a&#x5B;1] - b&#x5B;1])\n\ndef astar(grid, start, goal):\n    open_set = &#x5B;]\n    start_node = Node(start)\n    goal_node = Node(goal)\n    heapq.heappush(open_set, start_node)\n\n    while open_set:\n        current = heapq.heappop(open_set)\n\n        if current.pos == goal_node.pos:\n            path = &#x5B;]\n            while current:\n                path.append(current.pos)\n                current = current.parent\n            return path&#x5B;::-1]\n\n        closed = set()\n\n        for dx, dy in &#x5B;(-1, 0), (1, 0), (0, -1), (0, 1)]:\n            nx, ny = current.pos&#x5B;0] + dx, current.pos&#x5B;1] + dy\n            if not (0 &lt;= nx &lt; len(grid) and 0 &lt;= ny &lt; len(grid&#x5B;0]):\n                continue\n            if grid&#x5B;nx]&#x5B;ny] == 1:  # obstacle\n                continue\n            neighbor_pos = (nx, ny)\n            if neighbor_pos in closed:\n                continue\n\n            neighbor = Node(neighbor_pos, current)\n            neighbor.g = current.g + 1\n            neighbor.h = manhattan(neighbor.pos, goal_node.pos)\n            neighbor.f = neighbor.g + neighbor.h\n\n            heapq.heappush(open_set, neighbor)\n            closed.add(neighbor_pos)\n\n    return None\n\n# 0 = walkable, 1 = obstacle\ngrid = &#x5B;\n    &#x5B;0, 0, 0, 0, 0],\n    &#x5B;0, 1, 1, 0, 0],\n    &#x5B;0, 0, 0, 0, 0],\n    &#x5B;0, 0, 1, 1, 0],\n    &#x5B;0, 0, 0, 0, 0],\n]\nstart = (0, 0)\ngoal = (4, 4)\npath = astar(grid, start, goal)\nprint(&quot;Path:&quot;, path)\nprint(&quot;Steps:&quot;, len(path) - 1 if path else &quot;No path&quot;)\n\n<\/pre><\/div>\n\n\n<p>The algorithm uses a min-heap (priority queue) ordered by f-score. Each node stores its position, parent pointer (for path reconstruction), g-cost, h-cost, and f-cost. The main loop pops the node with the lowest f-score, checks if it&#8217;s the goal, then generates valid neighbors (within bounds, not obstacles). If a neighbor hasn&#8217;t been visited, it gets added to the heap. When the goal is popped, the parent chain reconstructs the path.<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nPath: &#x5B;(0, 0), (1, 0), (2, 0), (2, 1), (2, 2), (3, 2), (4, 2), (4, 3), (4, 4)]\nSteps: 8\n\n<\/pre><\/div>\n\n\n<h2 class=\"wp-block-heading\">Solving the 8-Puzzle with A*<\/h2>\n\n\n\n<p>The 8-puzzle is a classic AI exercise &#8211; a 3&#215;3 grid with 8 numbered tiles and one empty space. You can slide tiles into the empty space. The goal is to arrange them in order. Let me implement an A* solver for this using the misplaced tiles heuristic.<\/p>\n\n\n\n<p>A* solver for the 8-puzzle using the misplaced tiles heuristic. The heuristic counts how many tiles are not in their goal position, providing an admissible estimate of the remaining cost.<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: python; title: ; notranslate\" title=\"\">\nfrom copy import deepcopy\nimport numpy as np\nimport time\n\ndef bestsolution(state):\n    bestsol = np.array(&#x5B;], int).reshape(-1, 9)\n    count = len(state) - 1\n    while count != -1:\n        bestsol = np.insert(bestsol, 0, state&#x5B;count]&#x5B;&quot;puzzle&quot;], 0)\n        count = state&#x5B;count]&#x5B;&quot;parent&quot;]\n    return bestsol.reshape(-1, 3, 3)\n\ndef misplaced_tiles(puzzle, goal):\n    mscost = np.sum(puzzle != goal) - 1\n    return mscost if mscost &gt; 0 else 0\n\ndef coordinates(puzzle):\n    pos = np.array(range(9))\n    for p, q in enumerate(puzzle):\n        pos&#x5B;q] = p\n    return pos\n\ndef evaluvate_misplaced(puzzle, goal):\n    steps = np.array(\n        &#x5B;(&quot;up&quot;, &#x5B;0, 1, 2], -3), (&quot;down&quot;, &#x5B;6, 7, 8], 3),\n         (&quot;left&quot;, &#x5B;0, 3, 6], -1), (&quot;right&quot;, &#x5B;2, 5, 8], 1)],\n        dtype=&#x5B;(&quot;move&quot;, str, 1), (&quot;position&quot;, list), (&quot;head&quot;, int)],\n    )\n    dtstate = &#x5B;(&quot;puzzle&quot;, list), (&quot;parent&quot;, int), (&quot;gn&quot;, int), (&quot;hn&quot;, int)]\n    costg = coordinates(goal)\n    parent = -1\n    gn = 0\n    hn = misplaced_tiles(coordinates(puzzle), costg)\n    state = np.array(&#x5B;(puzzle, parent, gn, hn)], dtstate)\n    dtpriority = &#x5B;(&quot;position&quot;, int), (&quot;fn&quot;, int)]\n    priority = np.array(&#x5B;(0, hn)], dtpriority)\n\n    while True:\n        priority = np.sort(priority, kind=&quot;mergesort&quot;, order=&#x5B;&quot;fn&quot;, &quot;position&quot;])\n        position, fn = priority&#x5B;0]\n        priority = np.delete(priority, 0, 0)\n        puzzle, parent, gn, hn = state&#x5B;position]\n        puzzle = np.array(puzzle)\n        blank = int(np.where(puzzle == 0)&#x5B;0])\n        gn = gn + 1\n        start_time = time.time()\n\n        for s in steps:\n            if blank not in s&#x5B;&quot;position&quot;]:\n                openstates = deepcopy(puzzle)\n                openstates&#x5B;blank], openstates&#x5B;blank + s&#x5B;&quot;head&quot;]] = \\\n                    openstates&#x5B;blank + s&#x5B;&quot;head&quot;]], openstates&#x5B;blank]\n\n                if not (np.all(list(state&#x5B;&quot;puzzle&quot;]) == openstates, 1)).any():\n                    end_time = time.time()\n                    if (end_time - start_time) &gt; 2:\n                        print(&quot;The 8 puzzle is unsolvable&quot;)\n                        break\n\n                    hn = misplaced_tiles(coordinates(openstates), costg)\n                    q = np.array(&#x5B;(openstates, position, gn, hn)], dtstate)\n                    state = np.append(state, q, 0)\n                    fn = gn + hn\n                    q = np.array(&#x5B;(len(state) - 1, fn)], dtpriority)\n                    priority = np.append(priority, q, 0)\n\n                    if np.array_equal(openstates, goal):\n                        print(&quot;The 8 puzzle is solvable&quot;)\n                        return state, len(priority)\n\n    return state, len(priority)\n\n# Initial state: 2 8 3 \/ 1 6 4 \/ 7 0 5\npuzzle = &#x5B;2, 8, 3, 1, 6, 4, 7, 0, 5]\n# Goal state: 1 2 3 \/ 8 0 4 \/ 7 6 5\ngoal = &#x5B;1, 2, 3, 8, 0, 4, 7, 6, 5]\n\nstate, visited = evaluvate_misplaced(puzzle, goal)\nbestpath = bestsolution(state)\n\nfor row in bestpath:\n    print(&quot; &quot;.join(str(t) for t in row))\n\ntotalmoves = len(bestpath) - 1\nprint(f&quot;Steps to reach goal: {totalmoves}&quot;)\nprint(f&quot;Total nodes visited: {len(state) - visited}&quot;)\n\n<\/pre><\/div>\n\n\n<p>The implementation uses structured NumPy arrays to store state efficiently. The misplaced_tiles function calculates the heuristic by counting tiles not in their final position. The main loop extracts the lowest f-score node from the priority queue, generates all valid moves (up\/down\/left\/right by swapping the blank tile), and adds new states to the queue if they haven&#8217;t been seen before. The parent pointers allow path reconstruction once the goal is reached.<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nThe 8 puzzle is solvable\n2 8 3\n1 6 4\n7 0 5\n\n2 8 3\n1 0 4\n7 6 5\n\n2 0 3\n1 8 4\n7 6 5\n\n0 2 3\n1 8 4\n7 6 5\n\n1 2 3\n0 8 4\n7 6 5\n\n1 2 3\n8 0 4\n7 6 5\n\nSteps to reach goal: 5\nTotal nodes visited: 6\n\n<\/pre><\/div>\n\n\n<h2 class=\"wp-block-heading\">When to Use A*<\/h2>\n\n\n\n<p>A* is the right choice when you have a graph or grid, you know the cost of moving between nodes, you can estimate the remaining cost to the goal, and you need the shortest path.<\/p>\n\n\n\n<p><strong>Game development<\/strong> is the most common use case. Real-time strategy games use A* for unit pathfinding. Tower defense games use it to place towers within range of enemy paths. Even simple games like Pac-Man use variants of A* for ghost movement.<\/p>\n\n\n\n<p><strong>GPS and navigation<\/strong> systems rely on A* for route planning. The road network is the graph, edge costs are travel times, and the heuristic is distance-as-the-crow-flies. Traffic-aware variants adjust edge costs in real time.<\/p>\n\n\n\n<p><strong>Robotics and motion planning<\/strong> use A* in configuration space &#8211; the robot&#8217;s possible positions become graph nodes, with edges representing valid movements. This lets robots plan collision-free paths through complex environments.<\/p>\n\n\n\n<p>A* is overkill when the search space is very small (just use BFS), when no admissible heuristic exists, or when any path is acceptable (use greedy best-first).<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">FAQ<\/h2>\n\n\n\n<p><strong>Q: What happens if h(N) overestimates the true cost?<\/strong><\/p>\n\n\n\n<p>If the heuristic overestimates, A* may find a suboptimal path. The algorithm no longer guarantees optimality. Heuristics that overestimate are called inadmissible, and they make A* behave more like greedy best-first search.<\/p>\n\n\n\n<p><strong>Q: Can A* handle moving obstacles?<\/strong><\/p>\n\n\n\n<p>The standard A* algorithm assumes a static graph. For moving obstacles, there are extensions like D* (Dynamic A*) that replan efficiently when the environment changes. Temporal constraints add significant complexity to pathfinding.<\/p>\n\n\n\n<p><strong>Q: Is A* faster than Dijkstra?<\/strong><\/p>\n\n\n\n<p>Yes, when a good heuristic is available. A* expands fewer nodes than Dijkstra because the heuristic guides the search. The improvement depends on the quality of the heuristic &#8211; a tight lower bound like Manhattan distance on a grid produces much faster search than a loose bound.<\/p>\n\n\n\n<p><strong>Q: What is the memory complexity of A*?<\/strong><\/p>\n\n\n\n<p>A* stores all visited nodes in the closed set and all frontier nodes in the open set. In the worst case, this is O(|V|) where V is the number of nodes in the graph. For large search spaces, this can be significant &#8211; IDA* (iterative deepening A*) uses less memory by trading speed for space.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Summary<\/h2>\n\n\n\n<p>The A* Algorithm is a cornerstone of pathfinding in computer science. Its combination of actual path cost and heuristic estimate gives it the best of both worlds &#8211; the guarantee of Dijkstra&#8217;s optimality with the speed of guided search. The key to using A* effectively is choosing an admissible heuristic that is as tight as possible to the true remaining cost. With the right heuristic, A* can solve pathfinding problems orders of magnitude faster than blind search.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>In this article, let me walk you through the A* Algorithm &#8211; one of the most widely used pathfinding algorithms in computer science. I first encountered it while building a game AI, and I keep coming back to it because it solves such a broad range of problems so cleanly. At its core, A* finds [&hellip;]<\/p>\n","protected":false},"author":41,"featured_media":36166,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[9],"tags":[],"class_list":["post-36163","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-examples"],"blocksy_meta":[],"_links":{"self":[{"href":"https:\/\/www.askpython.com\/wp-json\/wp\/v2\/posts\/36163","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.askpython.com\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.askpython.com\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.askpython.com\/wp-json\/wp\/v2\/users\/41"}],"replies":[{"embeddable":true,"href":"https:\/\/www.askpython.com\/wp-json\/wp\/v2\/comments?post=36163"}],"version-history":[{"count":0,"href":"https:\/\/www.askpython.com\/wp-json\/wp\/v2\/posts\/36163\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.askpython.com\/wp-json\/wp\/v2\/media\/36166"}],"wp:attachment":[{"href":"https:\/\/www.askpython.com\/wp-json\/wp\/v2\/media?parent=36163"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.askpython.com\/wp-json\/wp\/v2\/categories?post=36163"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.askpython.com\/wp-json\/wp\/v2\/tags?post=36163"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}