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
98Time Complexity: O(b^(d/2)) where b is the branching factor and d is the depth
109Space Complexity: O(b^(d/2))
10+
11+ https://en.wikipedia.org/wiki/Bidirectional_search
1112"""
1213
1314from collections import deque
14- from typing import Dict , List , Optional , Set , Tuple
15+ from typing import Dict , List , Optional
1516
1617
1718def 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
180172if __name__ == "__main__" :
0 commit comments