mirror of
https://github.com/TheAlgorithms/Python.git
synced 2025-07-04 16:57:32 +08:00

* 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>
202 lines
5.5 KiB
Python
202 lines
5.5 KiB
Python
"""
|
|
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()
|