Skip to content

Commit 7804180

Browse files
committed
Add bidirectional search algorithm implementation
1 parent 74b540a commit 7804180

1 file changed

Lines changed: 181 additions & 0 deletions

File tree

graphs/bidirectional_search.py

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
"""
2+
Bidirectional Search Algorithm
3+
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.
8+
9+
Time Complexity: O(b^(d/2)) where b is the branching factor and d is the depth
10+
Space Complexity: O(b^(d/2))
11+
"""
12+
13+
from collections import deque
14+
from typing import Dict, List, Optional, Set, Tuple
15+
16+
17+
def bidirectional_search(
18+
graph: Dict[int, List[int]], start: int, goal: int
19+
) -> Optional[List[int]]:
20+
"""
21+
Perform bidirectional search on a graph to find the shortest path
22+
between start and goal nodes.
23+
24+
Args:
25+
graph: A dictionary where keys are nodes and values are lists of adjacent nodes
26+
start: The starting node
27+
goal: The target node
28+
29+
Returns:
30+
A list representing the path from start to goal, or None if no path exists
31+
"""
32+
if start == goal:
33+
return [start]
34+
35+
# Check if start and goal are in the graph
36+
if start not in graph or goal not in graph:
37+
return None
38+
39+
# Initialize forward and backward search queues
40+
forward_queue = deque([(start, [start])])
41+
backward_queue = deque([(goal, [goal])])
42+
43+
# Initialize visited sets for both directions
44+
forward_visited: Set[int] = {start}
45+
backward_visited: Set[int] = {goal}
46+
47+
# Dictionary to store paths
48+
forward_paths: Dict[int, List[int]] = {start: [start]}
49+
backward_paths: Dict[int, List[int]] = {goal: [goal]}
50+
51+
while forward_queue and backward_queue:
52+
# 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:
91+
return None
92+
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))
101+
102+
# Check if the neighbor is in the other visited set (intersection)
103+
if neighbor in other_visited:
104+
return neighbor
105+
106+
return None
107+
108+
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+
"""
138+
# Example graph represented as an adjacency list
139+
graph = {
140+
0: [1, 2],
141+
1: [0, 3, 4],
142+
2: [0, 5, 6],
143+
3: [1, 7],
144+
4: [1, 8],
145+
5: [2, 9],
146+
6: [2, 10],
147+
7: [3, 11],
148+
8: [4, 11],
149+
9: [5, 11],
150+
10: [6, 11],
151+
11: [7, 8, 9, 10],
152+
}
153+
154+
# Test case 1: Path exists
155+
start, goal = 0, 11
156+
path = bidirectional_search(graph, start, goal)
157+
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
159+
160+
# Test case 2: Start and goal are the same
161+
start, goal = 5, 5
162+
path = bidirectional_search(graph, start, goal)
163+
print(f"Path from {start} to {goal}: {path}")
164+
# Expected: Path from 5 to 5: [5]
165+
166+
# Test case 3: No path exists (disconnected graph)
167+
disconnected_graph = {
168+
0: [1, 2],
169+
1: [0],
170+
2: [0],
171+
3: [4],
172+
4: [3],
173+
}
174+
start, goal = 0, 3
175+
path = bidirectional_search(disconnected_graph, start, goal)
176+
print(f"Path from {start} to {goal}: {path}")
177+
# Expected: Path from 0 to 3: None
178+
179+
180+
if __name__ == "__main__":
181+
main()

0 commit comments

Comments
 (0)