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