mirror of
https://github.com/TheAlgorithms/Python.git
synced 2025-05-31 04:27:01 +00:00
Add bidirectional search algorithm implementation (#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>
This commit is contained in:
parent
26ad6891d8
commit
ca445f5296
201
graphs/bidirectional_search.py
Normal file
201
graphs/bidirectional_search.py
Normal file
@ -0,0 +1,201 @@
|
||||
"""
|
||||
Bidirectional Search Algorithm.
|
||||
|
||||
This algorithm searches from both the source and target nodes simultaneously,
|
||||
meeting somewhere in the middle. This approach can significantly reduce the
|
||||
search space compared to a traditional one-directional search.
|
||||
|
||||
Time Complexity: O(b^(d/2)) where b is the branching factor and d is the depth
|
||||
Space Complexity: O(b^(d/2))
|
||||
|
||||
https://en.wikipedia.org/wiki/Bidirectional_search
|
||||
"""
|
||||
|
||||
from collections import deque
|
||||
|
||||
|
||||
def expand_search(
|
||||
graph: dict[int, list[int]],
|
||||
queue: deque[int],
|
||||
parents: dict[int, int | None],
|
||||
opposite_direction_parents: dict[int, int | None],
|
||||
) -> int | None:
|
||||
if not queue:
|
||||
return None
|
||||
|
||||
current = queue.popleft()
|
||||
for neighbor in graph[current]:
|
||||
if neighbor in parents:
|
||||
continue
|
||||
|
||||
parents[neighbor] = current
|
||||
queue.append(neighbor)
|
||||
|
||||
# Check if this creates an intersection
|
||||
if neighbor in opposite_direction_parents:
|
||||
return neighbor
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def construct_path(current: int | None, parents: dict[int, int | None]) -> list[int]:
|
||||
path: list[int] = []
|
||||
while current is not None:
|
||||
path.append(current)
|
||||
current = parents[current]
|
||||
return path
|
||||
|
||||
|
||||
def bidirectional_search(
|
||||
graph: dict[int, list[int]], start: int, goal: int
|
||||
) -> list[int] | None:
|
||||
"""
|
||||
Perform bidirectional search on a graph to find the shortest path.
|
||||
|
||||
Args:
|
||||
graph: A dictionary where keys are nodes and values are lists of adjacent nodes
|
||||
start: The starting node
|
||||
goal: The target node
|
||||
|
||||
Returns:
|
||||
A list representing the path from start to goal, or None if no path exists
|
||||
|
||||
Examples:
|
||||
>>> graph = {
|
||||
... 0: [1, 2],
|
||||
... 1: [0, 3, 4],
|
||||
... 2: [0, 5, 6],
|
||||
... 3: [1, 7],
|
||||
... 4: [1, 8],
|
||||
... 5: [2, 9],
|
||||
... 6: [2, 10],
|
||||
... 7: [3, 11],
|
||||
... 8: [4, 11],
|
||||
... 9: [5, 11],
|
||||
... 10: [6, 11],
|
||||
... 11: [7, 8, 9, 10],
|
||||
... }
|
||||
>>> bidirectional_search(graph=graph, start=0, goal=11)
|
||||
[0, 1, 3, 7, 11]
|
||||
>>> bidirectional_search(graph=graph, start=5, goal=5)
|
||||
[5]
|
||||
>>> disconnected_graph = {
|
||||
... 0: [1, 2],
|
||||
... 1: [0],
|
||||
... 2: [0],
|
||||
... 3: [4],
|
||||
... 4: [3],
|
||||
... }
|
||||
>>> bidirectional_search(graph=disconnected_graph, start=0, goal=3) is None
|
||||
True
|
||||
"""
|
||||
if start == goal:
|
||||
return [start]
|
||||
|
||||
# Check if start and goal are in the graph
|
||||
if start not in graph or goal not in graph:
|
||||
return None
|
||||
|
||||
# Initialize forward and backward search dictionaries
|
||||
# Each maps a node to its parent in the search
|
||||
forward_parents: dict[int, int | None] = {start: None}
|
||||
backward_parents: dict[int, int | None] = {goal: None}
|
||||
|
||||
# Initialize forward and backward search queues
|
||||
forward_queue = deque([start])
|
||||
backward_queue = deque([goal])
|
||||
|
||||
# Intersection node (where the two searches meet)
|
||||
intersection = None
|
||||
|
||||
# Continue until both queues are empty or an intersection is found
|
||||
while forward_queue and backward_queue and intersection is None:
|
||||
# Expand forward search
|
||||
intersection = expand_search(
|
||||
graph=graph,
|
||||
queue=forward_queue,
|
||||
parents=forward_parents,
|
||||
opposite_direction_parents=backward_parents,
|
||||
)
|
||||
|
||||
# If no intersection found, expand backward search
|
||||
if intersection is not None:
|
||||
break
|
||||
|
||||
intersection = expand_search(
|
||||
graph=graph,
|
||||
queue=backward_queue,
|
||||
parents=backward_parents,
|
||||
opposite_direction_parents=forward_parents,
|
||||
)
|
||||
|
||||
# If no intersection found, there's no path
|
||||
if intersection is None:
|
||||
return None
|
||||
|
||||
# Construct path from start to intersection
|
||||
forward_path: list[int] = construct_path(
|
||||
current=intersection, parents=forward_parents
|
||||
)
|
||||
forward_path.reverse()
|
||||
|
||||
# Construct path from intersection to goal
|
||||
backward_path: list[int] = construct_path(
|
||||
current=backward_parents[intersection], parents=backward_parents
|
||||
)
|
||||
|
||||
# Return the complete path
|
||||
return forward_path + backward_path
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""
|
||||
Run example of bidirectional search algorithm.
|
||||
|
||||
Examples:
|
||||
>>> main() # doctest: +NORMALIZE_WHITESPACE
|
||||
Path from 0 to 11: [0, 1, 3, 7, 11]
|
||||
Path from 5 to 5: [5]
|
||||
Path from 0 to 3: None
|
||||
"""
|
||||
# Example graph represented as an adjacency list
|
||||
example_graph = {
|
||||
0: [1, 2],
|
||||
1: [0, 3, 4],
|
||||
2: [0, 5, 6],
|
||||
3: [1, 7],
|
||||
4: [1, 8],
|
||||
5: [2, 9],
|
||||
6: [2, 10],
|
||||
7: [3, 11],
|
||||
8: [4, 11],
|
||||
9: [5, 11],
|
||||
10: [6, 11],
|
||||
11: [7, 8, 9, 10],
|
||||
}
|
||||
|
||||
# Test case 1: Path exists
|
||||
start, goal = 0, 11
|
||||
path = bidirectional_search(graph=example_graph, start=start, goal=goal)
|
||||
print(f"Path from {start} to {goal}: {path}")
|
||||
|
||||
# Test case 2: Start and goal are the same
|
||||
start, goal = 5, 5
|
||||
path = bidirectional_search(graph=example_graph, start=start, goal=goal)
|
||||
print(f"Path from {start} to {goal}: {path}")
|
||||
|
||||
# Test case 3: No path exists (disconnected graph)
|
||||
disconnected_graph = {
|
||||
0: [1, 2],
|
||||
1: [0],
|
||||
2: [0],
|
||||
3: [4],
|
||||
4: [3],
|
||||
}
|
||||
start, goal = 0, 3
|
||||
path = bidirectional_search(graph=disconnected_graph, start=start, goal=goal)
|
||||
print(f"Path from {start} to {goal}: {path}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
Loading…
x
Reference in New Issue
Block a user