Skip to content

Commit 29f9da7

Browse files
Stop bidirectional A* when either frontier is empty (#15484)
* Stop bidirectional A* when either frontier is empty * Add bidirectional A* regressions and a grid benchmark Preserve all 18 existing doctest examples and append frontier exhaustion regressions and controls. Add an optional 2,025-cell benchmark with a documented 89-cell reachable corridor, leaving the production fix unchanged.
1 parent 84b73d0 commit 29f9da7

1 file changed

Lines changed: 53 additions & 1 deletion

File tree

‎graphs/bidirectional_a_star.py‎

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
"""
22
https://en.wikipedia.org/wiki/Bidirectional_search
3+
4+
Run this file with --benchmark to time a search on a 45 by 45 grid.
5+
The grid has 2,025 cells, with a single 89-cell staircase corridor.
36
"""
47

58
from __future__ import annotations
@@ -179,6 +182,25 @@ class BidirectionalAStar:
179182
>>> bd_astar.search() # doctest: +NORMALIZE_WHITESPACE
180183
[(0, 0), (0, 1), (0, 2), (1, 2), (1, 3), (2, 3), (2, 4),
181184
(2, 5), (3, 5), (4, 5), (5, 5), (5, 6), (6, 6)]
185+
186+
An isolated endpoint exhausts one frontier before the other.
187+
>>> original_grid = grid[:]
188+
>>> grid[:] = [[0, 1, 0], [1, 1, 0]]
189+
>>> BidirectionalAStar((0, 0), (0, 2)).search()
190+
[(0, 0)]
191+
>>> BidirectionalAStar((0, 2), (0, 0)).search()
192+
[(0, 2)]
193+
194+
Both frontiers can also empty together, without a path.
195+
>>> grid[:] = [[0, 1, 0]]
196+
>>> BidirectionalAStar((0, 0), (0, 2)).search()
197+
[(0, 0)]
198+
199+
A reachable corridor still returns the complete path.
200+
>>> grid[:] = [[0, 0, 0]]
201+
>>> BidirectionalAStar((0, 0), (0, 2)).search()
202+
[(0, 0), (0, 1), (0, 2)]
203+
>>> grid[:] = original_grid
182204
"""
183205

184206
def __init__(self, start: TPosition, goal: TPosition) -> None:
@@ -187,7 +209,7 @@ def __init__(self, start: TPosition, goal: TPosition) -> None:
187209
self.reached = False
188210

189211
def search(self) -> list[TPosition]:
190-
while self.fwd_astar.open_nodes or self.bwd_astar.open_nodes:
212+
while self.fwd_astar.open_nodes and self.bwd_astar.open_nodes:
191213
self.fwd_astar.open_nodes.sort()
192214
self.bwd_astar.open_nodes.sort()
193215
current_fwd_node = self.fwd_astar.open_nodes.pop(0)
@@ -257,3 +279,33 @@ def retrace_bidirectional_path(
257279
bidir_astar = BidirectionalAStar(init, goal)
258280
bd_end_time = time.time() - bd_start_time
259281
print(f"BidirectionalAStar execution time = {bd_end_time:f} seconds")
282+
283+
import sys
284+
285+
if "--benchmark" in sys.argv:
286+
from timeit import repeat
287+
288+
# A single staircase corridor gives both versions the same reachable path.
289+
# Keep grid construction and the correctness check outside the timing.
290+
size = 45
291+
grid = [[1] * size for _ in range(size)]
292+
expected_path = []
293+
for offset in range(size):
294+
grid[offset][offset] = 0
295+
expected_path.append((offset, offset))
296+
if offset < size - 1:
297+
grid[offset][offset + 1] = 0
298+
expected_path.append((offset, offset + 1))
299+
init, goal = (0, 0), (size - 1, size - 1)
300+
assert BidirectionalAStar(init, goal).search() == expected_path
301+
302+
searches = 100
303+
samples = repeat(
304+
"BidirectionalAStar(init, goal).search()",
305+
globals=globals(),
306+
number=searches,
307+
repeat=5,
308+
)
309+
print(f"Benchmark: {size * size} cells, {len(expected_path)} traversable")
310+
print(f"Seconds per search ({searches} searches per sample):")
311+
print([sample / searches for sample in samples])

0 commit comments

Comments
 (0)