Skip to content

Commit f71b167

Browse files
committed
Fix style and linting issues in bidirectional search
1 parent 7804180 commit f71b167

File tree

1 file changed

+95
-103
lines changed

1 file changed

+95
-103
lines changed

Diff for: graphs/bidirectional_search.py

+95-103
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,25 @@
11
"""
2-
Bidirectional Search Algorithm
2+
Bidirectional Search Algorithm.
33
4-
A bidirectional search algorithm searches from both the source and the target
5-
simultaneously, meeting somewhere in the middle. This can significantly reduce
6-
the search space and improve performance compared to a single-direction search
7-
in many scenarios.
4+
This algorithm searches from both the source and target nodes simultaneously,
5+
meeting somewhere in the middle. This approach can significantly reduce the
6+
search space compared to a traditional one-directional search.
87
98
Time Complexity: O(b^(d/2)) where b is the branching factor and d is the depth
109
Space Complexity: O(b^(d/2))
10+
11+
https://en.wikipedia.org/wiki/Bidirectional_search
1112
"""
1213

1314
from collections import deque
14-
from typing import Dict, List, Optional, Set, Tuple
15+
from typing import Dict, List, Optional
1516

1617

1718
def bidirectional_search(
1819
graph: Dict[int, List[int]], start: int, goal: int
1920
) -> Optional[List[int]]:
2021
"""
21-
Perform bidirectional search on a graph to find the shortest path
22-
between start and goal nodes.
22+
Perform bidirectional search on a graph to find the shortest path.
2323
2424
Args:
2525
graph: A dictionary where keys are nodes and values are lists of adjacent nodes
@@ -28,6 +28,35 @@ def bidirectional_search(
2828
2929
Returns:
3030
A list representing the path from start to goal, or None if no path exists
31+
32+
Examples:
33+
>>> graph = {
34+
... 0: [1, 2],
35+
... 1: [0, 3, 4],
36+
... 2: [0, 5, 6],
37+
... 3: [1, 7],
38+
... 4: [1, 8],
39+
... 5: [2, 9],
40+
... 6: [2, 10],
41+
... 7: [3, 11],
42+
... 8: [4, 11],
43+
... 9: [5, 11],
44+
... 10: [6, 11],
45+
... 11: [7, 8, 9, 10],
46+
... }
47+
>>> bidirectional_search(graph, 0, 11)
48+
[0, 1, 3, 7, 11]
49+
>>> bidirectional_search(graph, 5, 5)
50+
[5]
51+
>>> disconnected_graph = {
52+
... 0: [1, 2],
53+
... 1: [0],
54+
... 2: [0],
55+
... 3: [4],
56+
... 4: [3],
57+
... }
58+
>>> bidirectional_search(disconnected_graph, 0, 3) is None
59+
True
3160
"""
3261
if start == goal:
3362
return [start]
@@ -36,107 +65,73 @@ def bidirectional_search(
3665
if start not in graph or goal not in graph:
3766
return None
3867

39-
# Initialize forward and backward search queues
40-
forward_queue = deque([(start, [start])])
41-
backward_queue = deque([(goal, [goal])])
68+
# Initialize forward and backward search dictionaries
69+
# Each maps a node to its parent in the search
70+
forward_parents = {start: None}
71+
backward_parents = {goal: None}
4272

43-
# Initialize visited sets for both directions
44-
forward_visited: Set[int] = {start}
45-
backward_visited: Set[int] = {goal}
73+
# Initialize forward and backward search queues
74+
forward_queue = deque([start])
75+
backward_queue = deque([goal])
4676

47-
# Dictionary to store paths
48-
forward_paths: Dict[int, List[int]] = {start: [start]}
49-
backward_paths: Dict[int, List[int]] = {goal: [goal]}
77+
# Intersection node (where the two searches meet)
78+
intersection = None
5079

51-
while forward_queue and backward_queue:
80+
# Continue until both queues are empty or an intersection is found
81+
while forward_queue and backward_queue and intersection is None:
5282
# Expand forward search
53-
intersection = expand_search(
54-
graph, forward_queue, forward_visited, forward_paths, backward_visited
55-
)
56-
if intersection:
57-
return construct_path(intersection, forward_paths, backward_paths)
58-
59-
# Expand backward search
60-
intersection = expand_search(
61-
graph, backward_queue, backward_visited, backward_paths, forward_visited
62-
)
63-
if intersection:
64-
return construct_path(intersection, forward_paths, backward_paths)
65-
66-
# No path found
67-
return None
68-
69-
70-
def expand_search(
71-
graph: Dict[int, List[int]],
72-
queue: deque,
73-
visited: Set[int],
74-
paths: Dict[int, List[int]],
75-
other_visited: Set[int],
76-
) -> Optional[int]:
77-
"""
78-
Expand the search in one direction and check for intersection.
79-
80-
Args:
81-
graph: The graph
82-
queue: The queue for this direction
83-
visited: Set of visited nodes for this direction
84-
paths: Dictionary to store paths for this direction
85-
other_visited: Set of visited nodes for the other direction
86-
87-
Returns:
88-
The intersection node if found, None otherwise
89-
"""
90-
if not queue:
83+
if forward_queue:
84+
current = forward_queue.popleft()
85+
for neighbor in graph[current]:
86+
if neighbor not in forward_parents:
87+
forward_parents[neighbor] = current
88+
forward_queue.append(neighbor)
89+
90+
# Check if this creates an intersection
91+
if neighbor in backward_parents:
92+
intersection = neighbor
93+
break
94+
95+
# If no intersection found, expand backward search
96+
if intersection is None and backward_queue:
97+
current = backward_queue.popleft()
98+
for neighbor in graph[current]:
99+
if neighbor not in backward_parents:
100+
backward_parents[neighbor] = current
101+
backward_queue.append(neighbor)
102+
103+
# Check if this creates an intersection
104+
if neighbor in forward_parents:
105+
intersection = neighbor
106+
break
107+
108+
# If no intersection found, there's no path
109+
if intersection is None:
91110
return None
92111

93-
current, path = queue.popleft()
94-
95-
for neighbor in graph[current]:
96-
if neighbor not in visited:
97-
visited.add(neighbor)
98-
new_path = path + [neighbor]
99-
paths[neighbor] = new_path
100-
queue.append((neighbor, new_path))
112+
# Construct path from start to intersection
113+
forward_path = []
114+
current = intersection
115+
while current is not None:
116+
forward_path.append(current)
117+
current = forward_parents[current]
118+
forward_path.reverse()
101119

102-
# Check if the neighbor is in the other visited set (intersection)
103-
if neighbor in other_visited:
104-
return neighbor
120+
# Construct path from intersection to goal
121+
backward_path = []
122+
current = backward_parents[intersection]
123+
while current is not None:
124+
backward_path.append(current)
125+
current = backward_parents[current]
105126

106-
return None
127+
# Return the complete path
128+
return forward_path + backward_path
107129

108130

109-
def construct_path(
110-
intersection: int, forward_paths: Dict[int, List[int]], backward_paths: Dict[int, List[int]]
111-
) -> List[int]:
112-
"""
113-
Construct the full path from the intersection point.
114-
115-
Args:
116-
intersection: The node where the two searches met
117-
forward_paths: Paths from start to intersection
118-
backward_paths: Paths from goal to intersection
119-
120-
Returns:
121-
The complete path from start to goal
122-
"""
123-
# Get the path from start to intersection
124-
forward_path = forward_paths[intersection]
125-
126-
# Get the path from goal to intersection and reverse it
127-
backward_path = backward_paths[intersection]
128-
backward_path.reverse()
129-
130-
# Combine the paths (remove the duplicate intersection node)
131-
return forward_path + backward_path[1:]
132-
133-
134-
def main():
135-
"""
136-
Example usage and test cases for bidirectional search
137-
"""
131+
def main() -> None:
132+
"""Run example of bidirectional search algorithm."""
138133
# Example graph represented as an adjacency list
139-
graph = {
134+
example_graph = {
140135
0: [1, 2],
141136
1: [0, 3, 4],
142137
2: [0, 5, 6],
@@ -153,15 +148,13 @@ def main():
153148

154149
# Test case 1: Path exists
155150
start, goal = 0, 11
156-
path = bidirectional_search(graph, start, goal)
151+
path = bidirectional_search(example_graph, start, goal)
157152
print(f"Path from {start} to {goal}: {path}")
158-
# Expected: Path from 0 to 11: [0, 1, 3, 7, 11] or similar valid shortest path
159153

160154
# Test case 2: Start and goal are the same
161155
start, goal = 5, 5
162-
path = bidirectional_search(graph, start, goal)
156+
path = bidirectional_search(example_graph, start, goal)
163157
print(f"Path from {start} to {goal}: {path}")
164-
# Expected: Path from 5 to 5: [5]
165158

166159
# Test case 3: No path exists (disconnected graph)
167160
disconnected_graph = {
@@ -174,7 +167,6 @@ def main():
174167
start, goal = 0, 3
175168
path = bidirectional_search(disconnected_graph, start, goal)
176169
print(f"Path from {start} to {goal}: {path}")
177-
# Expected: Path from 0 to 3: None
178170

179171

180172
if __name__ == "__main__":

0 commit comments

Comments
 (0)