Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
Skip to content

Commit ca445f5

Browse files
prajwalc22pre-commit-ci[bot]MaximSmolskiy
authored
Add bidirectional search algorithm implementation (TheAlgorithms#12649)
* Add bidirectional search algorithm implementation * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Fix style and linting issues in bidirectional search * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Add doctest for main function * Add doctest for main function * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fixed deprications * fixed deprications * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * removed unused import * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Update bidirectional_search.py * Update bidirectional_search.py * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Update bidirectional_search.py * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Maxim Smolskiy <mithridatus@mail.ru>
1 parent 26ad689 commit ca445f5

File tree

1 file changed

+201
-0
lines changed

1 file changed

+201
-0
lines changed

graphs/bidirectional_search.py

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
"""
2+
Bidirectional Search Algorithm.
3+
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.
7+
8+
Time Complexity: O(b^(d/2)) where b is the branching factor and d is the depth
9+
Space Complexity: O(b^(d/2))
10+
11+
https://en.wikipedia.org/wiki/Bidirectional_search
12+
"""
13+
14+
from collections import deque
15+
16+
17+
def expand_search(
18+
graph: dict[int, list[int]],
19+
queue: deque[int],
20+
parents: dict[int, int | None],
21+
opposite_direction_parents: dict[int, int | None],
22+
) -> int | None:
23+
if not queue:
24+
return None
25+
26+
current = queue.popleft()
27+
for neighbor in graph[current]:
28+
if neighbor in parents:
29+
continue
30+
31+
parents[neighbor] = current
32+
queue.append(neighbor)
33+
34+
# Check if this creates an intersection
35+
if neighbor in opposite_direction_parents:
36+
return neighbor
37+
38+
return None
39+
40+
41+
def construct_path(current: int | None, parents: dict[int, int | None]) -> list[int]:
42+
path: list[int] = []
43+
while current is not None:
44+
path.append(current)
45+
current = parents[current]
46+
return path
47+
48+
49+
def bidirectional_search(
50+
graph: dict[int, list[int]], start: int, goal: int
51+
) -> list[int] | None:
52+
"""
53+
Perform bidirectional search on a graph to find the shortest path.
54+
55+
Args:
56+
graph: A dictionary where keys are nodes and values are lists of adjacent nodes
57+
start: The starting node
58+
goal: The target node
59+
60+
Returns:
61+
A list representing the path from start to goal, or None if no path exists
62+
63+
Examples:
64+
>>> graph = {
65+
... 0: [1, 2],
66+
... 1: [0, 3, 4],
67+
... 2: [0, 5, 6],
68+
... 3: [1, 7],
69+
... 4: [1, 8],
70+
... 5: [2, 9],
71+
... 6: [2, 10],
72+
... 7: [3, 11],
73+
... 8: [4, 11],
74+
... 9: [5, 11],
75+
... 10: [6, 11],
76+
... 11: [7, 8, 9, 10],
77+
... }
78+
>>> bidirectional_search(graph=graph, start=0, goal=11)
79+
[0, 1, 3, 7, 11]
80+
>>> bidirectional_search(graph=graph, start=5, goal=5)
81+
[5]
82+
>>> disconnected_graph = {
83+
... 0: [1, 2],
84+
... 1: [0],
85+
... 2: [0],
86+
... 3: [4],
87+
... 4: [3],
88+
... }
89+
>>> bidirectional_search(graph=disconnected_graph, start=0, goal=3) is None
90+
True
91+
"""
92+
if start == goal:
93+
return [start]
94+
95+
# Check if start and goal are in the graph
96+
if start not in graph or goal not in graph:
97+
return None
98+
99+
# Initialize forward and backward search dictionaries
100+
# Each maps a node to its parent in the search
101+
forward_parents: dict[int, int | None] = {start: None}
102+
backward_parents: dict[int, int | None] = {goal: None}
103+
104+
# Initialize forward and backward search queues
105+
forward_queue = deque([start])
106+
backward_queue = deque([goal])
107+
108+
# Intersection node (where the two searches meet)
109+
intersection = None
110+
111+
# Continue until both queues are empty or an intersection is found
112+
while forward_queue and backward_queue and intersection is None:
113+
# Expand forward search
114+
intersection = expand_search(
115+
graph=graph,
116+
queue=forward_queue,
117+
parents=forward_parents,
118+
opposite_direction_parents=backward_parents,
119+
)
120+
121+
# If no intersection found, expand backward search
122+
if intersection is not None:
123+
break
124+
125+
intersection = expand_search(
126+
graph=graph,
127+
queue=backward_queue,
128+
parents=backward_parents,
129+
opposite_direction_parents=forward_parents,
130+
)
131+
132+
# If no intersection found, there's no path
133+
if intersection is None:
134+
return None
135+
136+
# Construct path from start to intersection
137+
forward_path: list[int] = construct_path(
138+
current=intersection, parents=forward_parents
139+
)
140+
forward_path.reverse()
141+
142+
# Construct path from intersection to goal
143+
backward_path: list[int] = construct_path(
144+
current=backward_parents[intersection], parents=backward_parents
145+
)
146+
147+
# Return the complete path
148+
return forward_path + backward_path
149+
150+
151+
def main() -> None:
152+
"""
153+
Run example of bidirectional search algorithm.
154+
155+
Examples:
156+
>>> main() # doctest: +NORMALIZE_WHITESPACE
157+
Path from 0 to 11: [0, 1, 3, 7, 11]
158+
Path from 5 to 5: [5]
159+
Path from 0 to 3: None
160+
"""
161+
# Example graph represented as an adjacency list
162+
example_graph = {
163+
0: [1, 2],
164+
1: [0, 3, 4],
165+
2: [0, 5, 6],
166+
3: [1, 7],
167+
4: [1, 8],
168+
5: [2, 9],
169+
6: [2, 10],
170+
7: [3, 11],
171+
8: [4, 11],
172+
9: [5, 11],
173+
10: [6, 11],
174+
11: [7, 8, 9, 10],
175+
}
176+
177+
# Test case 1: Path exists
178+
start, goal = 0, 11
179+
path = bidirectional_search(graph=example_graph, start=start, goal=goal)
180+
print(f"Path from {start} to {goal}: {path}")
181+
182+
# Test case 2: Start and goal are the same
183+
start, goal = 5, 5
184+
path = bidirectional_search(graph=example_graph, start=start, goal=goal)
185+
print(f"Path from {start} to {goal}: {path}")
186+
187+
# Test case 3: No path exists (disconnected graph)
188+
disconnected_graph = {
189+
0: [1, 2],
190+
1: [0],
191+
2: [0],
192+
3: [4],
193+
4: [3],
194+
}
195+
start, goal = 0, 3
196+
path = bidirectional_search(graph=disconnected_graph, start=start, goal=goal)
197+
print(f"Path from {start} to {goal}: {path}")
198+
199+
200+
if __name__ == "__main__":
201+
main()

0 commit comments

Comments
 (0)