1
1
"""
2
- Bidirectional Search Algorithm
2
+ Bidirectional Search Algorithm.
3
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.
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.
8
7
9
8
Time Complexity: O(b^(d/2)) where b is the branching factor and d is the depth
10
9
Space Complexity: O(b^(d/2))
10
+
11
+ https://en.wikipedia.org/wiki/Bidirectional_search
11
12
"""
12
13
13
14
from collections import deque
14
- from typing import Dict , List , Optional , Set , Tuple
15
+ from typing import Dict , List , Optional
15
16
16
17
17
18
def bidirectional_search (
18
19
graph : Dict [int , List [int ]], start : int , goal : int
19
20
) -> Optional [List [int ]]:
20
21
"""
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.
23
23
24
24
Args:
25
25
graph: A dictionary where keys are nodes and values are lists of adjacent nodes
@@ -28,6 +28,35 @@ def bidirectional_search(
28
28
29
29
Returns:
30
30
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
31
60
"""
32
61
if start == goal :
33
62
return [start ]
@@ -36,107 +65,73 @@ def bidirectional_search(
36
65
if start not in graph or goal not in graph :
37
66
return None
38
67
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 }
42
72
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 ])
46
76
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
50
79
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 :
52
82
# 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 :
91
110
return None
92
111
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 ()
101
119
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 ]
105
126
106
- return None
127
+ # Return the complete path
128
+ return forward_path + backward_path
107
129
108
130
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."""
138
133
# Example graph represented as an adjacency list
139
- graph = {
134
+ example_graph = {
140
135
0 : [1 , 2 ],
141
136
1 : [0 , 3 , 4 ],
142
137
2 : [0 , 5 , 6 ],
@@ -153,15 +148,13 @@ def main():
153
148
154
149
# Test case 1: Path exists
155
150
start , goal = 0 , 11
156
- path = bidirectional_search (graph , start , goal )
151
+ path = bidirectional_search (example_graph , start , goal )
157
152
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
153
160
154
# Test case 2: Start and goal are the same
161
155
start , goal = 5 , 5
162
- path = bidirectional_search (graph , start , goal )
156
+ path = bidirectional_search (example_graph , start , goal )
163
157
print (f"Path from { start } to { goal } : { path } " )
164
- # Expected: Path from 5 to 5: [5]
165
158
166
159
# Test case 3: No path exists (disconnected graph)
167
160
disconnected_graph = {
@@ -174,7 +167,6 @@ def main():
174
167
start , goal = 0 , 3
175
168
path = bidirectional_search (disconnected_graph , start , goal )
176
169
print (f"Path from { start } to { goal } : { path } " )
177
- # Expected: Path from 0 to 3: None
178
170
179
171
180
172
if __name__ == "__main__" :
0 commit comments