Pathfinding Algorithm
Bidirectional A*
“The Diplomat”
History
Proposed in the late 1960s, Bidirectional Search addresses the problem of exponential growth in search trees. The logic is simple geometry: the area of two small circles is smaller than the area of one big circle.
It essentially digs a tunnel from both sides of the mountain, hoping to meet exactly in the middle. When implemented correctly, it can drastically cut down compute time.
How It Works
This algorithm runs two simultaneous searches: one Forward from the start, and one Backward from the goal.
The Algorithm Step-by-Step
- Setup: Initialize two Priority Queues (Start-to-Goal and Goal-to-Start).
- Alternate: Advance the Forward search one step, then the Backward search one step.
- Check Intersection: After every step, check if the current node has been visited by the *other* search.
- Meet: Once the searches collide, we have a bridge.
- Reconstruct: Stitch the path from Start-to-Meeting-Point and Meeting-Point-to-Goal.
The Complexity
The hardest part isn't the search; it's the termination condition. Just because the searches met doesn't *automatically* mean that specific meeting point lies on the optimal path, though in unweighted graphs it usually does.
This is the killer feature. Instead of searching depth d, we search depth d/2 twice. Because graph expansion is exponential, this is a massive performance win.
We still need to store the frontiers for both searches.
The core data structure powering Bidirectional A*.
How this algorithm appears during visualization.
Code Walkthrough
Real implementations in multiple programming languages. Select a language to view idiomatic code.
1import heapq23def bidirectional_astar(grid: list[list[int]], start: tuple[int, int], end: tuple[int, int]) -> list[tuple[int, int]] | None:4 """Bidirectional A* search from both start and end."""5 rows, cols = len(grid), len(grid[0])6 directions = [(0, 1), (1, 0), (0, -1), (-1, 0)]78 # Forward search state9 g_fwd = {start: 0}10 parent_fwd = {start: None}11 open_fwd = [(heuristic(start, end), start)]12 closed_fwd = set()1314 # Backward search state15 g_bwd = {end: 0}16 parent_bwd = {end: None}17 open_bwd = [(heuristic(end, start), end)]18 closed_bwd = set()1920 best_path = None21 best_cost = float('inf')2223 while open_fwd and open_bwd:24 # Expand forward25 if open_fwd:26 _, curr = heapq.heappop(open_fwd)27 if curr not in closed_fwd:28 closed_fwd.add(curr)2930 if curr in closed_bwd:31 cost = g_fwd[curr] + g_bwd[curr]32 if cost < best_cost:33 best_cost = cost34 best_path = _reconstruct(curr, parent_fwd, parent_bwd)3536 for dr, dc in directions:37 nr, nc = curr[0] + dr, curr[1] + dc38 if 0 <= nr < rows and 0 <= nc < cols and grid[nr][nc] == 0:39 neighbor = (nr, nc)40 tentative_g = g_fwd[curr] + 141 if neighbor not in g_fwd or tentative_g < g_fwd[neighbor]:42 g_fwd[neighbor] = tentative_g43 parent_fwd[neighbor] = curr44 f = tentative_g + heuristic(neighbor, end)45 heapq.heappush(open_fwd, (f, neighbor))4647 # Expand backward (similar logic)48 if open_bwd:49 _, curr = heapq.heappop(open_bwd)50 if curr not in closed_bwd:51 closed_bwd.add(curr)5253 if curr in closed_fwd:54 cost = g_fwd[curr] + g_bwd[curr]55 if cost < best_cost:56 best_cost = cost57 best_path = _reconstruct(curr, parent_fwd, parent_bwd)5859 for dr, dc in directions:60 nr, nc = curr[0] + dr, curr[1] + dc61 if 0 <= nr < rows and 0 <= nc < cols and grid[nr][nc] == 0:62 neighbor = (nr, nc)63 tentative_g = g_bwd[curr] + 164 if neighbor not in g_bwd or tentative_g < g_bwd[neighbor]:65 g_bwd[neighbor] = tentative_g66 parent_bwd[neighbor] = curr67 f = tentative_g + heuristic(neighbor, start)68 heapq.heappush(open_bwd, (f, neighbor))6970 return best_path7172def _reconstruct(meeting, parent_fwd, parent_bwd):73 path_fwd = []74 curr = meeting75 while curr:76 path_fwd.append(curr)77 curr = parent_fwd[curr]78 path_fwd.reverse()7980 path_bwd = []81 curr = parent_bwd[meeting]82 while curr:83 path_bwd.append(curr)84 curr = parent_bwd[curr]8586 return path_fwd + path_bwdReal World Use Cases
- 1Social Network Connection (finding path between two users)
- 2Rubik's Cube solvers (meet-in-the-middle attacks)
- 3Large-scale map routing
- 4Word ladder puzzles
- 5Database relationship mapping
- 6Flight routing (hub-to-hub)
Key Insights
- It is significantly faster than standard A* for long paths.
- Implementation is more complex (managing two states).
- You must know the destination explicitly (can't search for 'nearest gas station').
- The graph must be traversable backwards (undirected or reversible edges).
- It visually demonstrates the power of divide-and-conquer.
When to Use
Use Bidirectional search for heavy-duty pathfinding on large graphs where you know the exact start and end points. It is the secret sauce for high-performance routing engines.
When NOT to Use
Avoid it if the graph is directed and cannot be reversed (like one-way streets where you can't calculate the backward path) or if the implementation complexity isn't worth the speed gain.