Skip to content

Commit a5c2467

Browse files
authored
Graphs : Bidirectional Breadth-First Search (#2057)
* implement bidirectional breadth first * remove useless import * remove trailing whitespaces
1 parent 4e8a0d9 commit a5c2467

File tree

1 file changed

+177
-0
lines changed

1 file changed

+177
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
"""
2+
https://en.wikipedia.org/wiki/Bidirectional_search
3+
"""
4+
5+
import time
6+
from typing import List, Tuple
7+
8+
grid = [
9+
[0, 0, 0, 0, 0, 0, 0],
10+
[0, 1, 0, 0, 0, 0, 0], # 0 are free path whereas 1's are obstacles
11+
[0, 0, 0, 0, 0, 0, 0],
12+
[0, 0, 1, 0, 0, 0, 0],
13+
[1, 0, 1, 0, 0, 0, 0],
14+
[0, 0, 0, 0, 0, 0, 0],
15+
[0, 0, 0, 0, 1, 0, 0],
16+
]
17+
18+
delta = [[-1, 0], [0, -1], [1, 0], [0, 1]] # up, left, down, right
19+
20+
21+
class Node:
22+
def __init__(self, pos_x, pos_y, goal_x, goal_y, parent):
23+
self.pos_x = pos_x
24+
self.pos_y = pos_y
25+
self.pos = (pos_y, pos_x)
26+
self.goal_x = goal_x
27+
self.goal_y = goal_y
28+
self.parent = parent
29+
30+
31+
class BreadthFirstSearch:
32+
"""
33+
>>> bfs = BreadthFirstSearch((0, 0), (len(grid) - 1, len(grid[0]) - 1))
34+
>>> (bfs.start.pos_y + delta[3][0], bfs.start.pos_x + delta[3][1])
35+
(0, 1)
36+
>>> [x.pos for x in bfs.get_successors(bfs.start)]
37+
[(1, 0), (0, 1)]
38+
>>> (bfs.start.pos_y + delta[2][0], bfs.start.pos_x + delta[2][1])
39+
(1, 0)
40+
>>> bfs.retrace_path(bfs.start)
41+
[(0, 0)]
42+
>>> bfs.search() # doctest: +NORMALIZE_WHITESPACE
43+
[(0, 0), (1, 0), (2, 0), (3, 0), (3, 1), (4, 1),
44+
(5, 1), (5, 2), (5, 3), (5, 4), (5, 5), (6, 5), (6, 6)]
45+
"""
46+
def __init__(self, start, goal):
47+
self.start = Node(start[1], start[0], goal[1], goal[0], None)
48+
self.target = Node(goal[1], goal[0], goal[1], goal[0], None)
49+
50+
self.node_queue = [self.start]
51+
self.reached = False
52+
53+
def search(self) -> List[Tuple[int]]:
54+
while self.node_queue:
55+
current_node = self.node_queue.pop(0)
56+
57+
if current_node.pos == self.target.pos:
58+
self.reached = True
59+
return self.retrace_path(current_node)
60+
61+
successors = self.get_successors(current_node)
62+
63+
for node in successors:
64+
self.node_queue.append(node)
65+
66+
if not (self.reached):
67+
return [(self.start.pos)]
68+
69+
def get_successors(self, parent: Node) -> List[Node]:
70+
"""
71+
Returns a list of successors (both in the grid and free spaces)
72+
"""
73+
successors = []
74+
for action in delta:
75+
pos_x = parent.pos_x + action[1]
76+
pos_y = parent.pos_y + action[0]
77+
if not (0 <= pos_x <= len(grid[0]) - 1 and 0 <= pos_y <= len(grid) - 1):
78+
continue
79+
80+
if grid[pos_y][pos_x] != 0:
81+
continue
82+
83+
successors.append(
84+
Node(pos_x, pos_y, self.target.pos_y, self.target.pos_x, parent)
85+
)
86+
return successors
87+
88+
def retrace_path(self, node: Node) -> List[Tuple[int]]:
89+
"""
90+
Retrace the path from parents to parents until start node
91+
"""
92+
current_node = node
93+
path = []
94+
while current_node is not None:
95+
path.append((current_node.pos_y, current_node.pos_x))
96+
current_node = current_node.parent
97+
path.reverse()
98+
return path
99+
100+
101+
class BidirectionalBreadthFirstSearch:
102+
"""
103+
>>> bd_bfs = BidirectionalBreadthFirstSearch((0, 0), (len(grid) - 1, len(grid[0]) - 1))
104+
>>> bd_bfs.fwd_bfs.start.pos == bd_bfs.bwd_bfs.target.pos
105+
True
106+
>>> bd_bfs.retrace_bidirectional_path(bd_bfs.fwd_bfs.start,
107+
... bd_bfs.bwd_bfs.start)
108+
[(0, 0)]
109+
>>> bd_bfs.search() # doctest: +NORMALIZE_WHITESPACE
110+
[(0, 0), (0, 1), (0, 2), (1, 2), (2, 2), (2, 3),
111+
(2, 4), (3, 4), (3, 5), (3, 6), (4, 6), (5, 6), (6, 6)]
112+
"""
113+
def __init__(self, start, goal):
114+
self.fwd_bfs = BreadthFirstSearch(start, goal)
115+
self.bwd_bfs = BreadthFirstSearch(goal, start)
116+
self.reached = False
117+
118+
def search(self) -> List[Tuple[int]]:
119+
while self.fwd_bfs.node_queue or self.bwd_bfs.node_queue:
120+
current_fwd_node = self.fwd_bfs.node_queue.pop(0)
121+
current_bwd_node = self.bwd_bfs.node_queue.pop(0)
122+
123+
if current_bwd_node.pos == current_fwd_node.pos:
124+
self.reached = True
125+
return self.retrace_bidirectional_path(
126+
current_fwd_node, current_bwd_node
127+
)
128+
129+
self.fwd_bfs.target = current_bwd_node
130+
self.bwd_bfs.target = current_fwd_node
131+
132+
successors = {
133+
self.fwd_bfs: self.fwd_bfs.get_successors(current_fwd_node),
134+
self.bwd_bfs: self.bwd_bfs.get_successors(current_bwd_node),
135+
}
136+
137+
for bfs in [self.fwd_bfs, self.bwd_bfs]:
138+
for node in successors[bfs]:
139+
bfs.node_queue.append(node)
140+
141+
if not self.reached:
142+
return [self.fwd_bfs.start.pos]
143+
144+
def retrace_bidirectional_path(
145+
self, fwd_node: Node, bwd_node: Node
146+
) -> List[Tuple[int]]:
147+
fwd_path = self.fwd_bfs.retrace_path(fwd_node)
148+
bwd_path = self.bwd_bfs.retrace_path(bwd_node)
149+
bwd_path.pop()
150+
bwd_path.reverse()
151+
path = fwd_path + bwd_path
152+
return path
153+
154+
155+
if __name__ == "__main__":
156+
# all coordinates are given in format [y,x]
157+
import doctest
158+
159+
doctest.testmod()
160+
init = (0, 0)
161+
goal = (len(grid) - 1, len(grid[0]) - 1)
162+
for elem in grid:
163+
print(elem)
164+
165+
start_bfs_time = time.time()
166+
bfs = BreadthFirstSearch(init, goal)
167+
path = bfs.search()
168+
bfs_time = time.time() - start_bfs_time
169+
170+
print("Unidirectional BFS computation time : ", bfs_time)
171+
172+
start_bd_bfs_time = time.time()
173+
bd_bfs = BidirectionalBreadthFirstSearch(init, goal)
174+
bd_path = bd_bfs.search()
175+
bd_bfs_time = time.time() - start_bd_bfs_time
176+
177+
print("Bidirectional BFS computation time : ", bd_bfs_time)

0 commit comments

Comments
 (0)