From 78041800fbd5cc697d182a9076e448e314755e07 Mon Sep 17 00:00:00 2001 From: prajwalc22 Date: Mon, 31 Mar 2025 22:59:17 +0530 Subject: [PATCH 01/12] Add bidirectional search algorithm implementation --- graphs/bidirectional_search.py | 181 +++++++++++++++++++++++++++++++++ 1 file changed, 181 insertions(+) create mode 100644 graphs/bidirectional_search.py diff --git a/graphs/bidirectional_search.py b/graphs/bidirectional_search.py new file mode 100644 index 000000000000..7d175ec6eb3a --- /dev/null +++ b/graphs/bidirectional_search.py @@ -0,0 +1,181 @@ +""" +Bidirectional Search Algorithm + +A bidirectional search algorithm searches from both the source and the target +simultaneously, meeting somewhere in the middle. This can significantly reduce +the search space and improve performance compared to a single-direction search +in many scenarios. + +Time Complexity: O(b^(d/2)) where b is the branching factor and d is the depth +Space Complexity: O(b^(d/2)) +""" + +from collections import deque +from typing import Dict, List, Optional, Set, Tuple + + +def bidirectional_search( + graph: Dict[int, List[int]], start: int, goal: int +) -> Optional[List[int]]: + """ + Perform bidirectional search on a graph to find the shortest path + between start and goal nodes. + + 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 + """ + 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 queues + forward_queue = deque([(start, [start])]) + backward_queue = deque([(goal, [goal])]) + + # Initialize visited sets for both directions + forward_visited: Set[int] = {start} + backward_visited: Set[int] = {goal} + + # Dictionary to store paths + forward_paths: Dict[int, List[int]] = {start: [start]} + backward_paths: Dict[int, List[int]] = {goal: [goal]} + + while forward_queue and backward_queue: + # Expand forward search + intersection = expand_search( + graph, forward_queue, forward_visited, forward_paths, backward_visited + ) + if intersection: + return construct_path(intersection, forward_paths, backward_paths) + + # Expand backward search + intersection = expand_search( + graph, backward_queue, backward_visited, backward_paths, forward_visited + ) + if intersection: + return construct_path(intersection, forward_paths, backward_paths) + + # No path found + return None + + +def expand_search( + graph: Dict[int, List[int]], + queue: deque, + visited: Set[int], + paths: Dict[int, List[int]], + other_visited: Set[int], +) -> Optional[int]: + """ + Expand the search in one direction and check for intersection. + + Args: + graph: The graph + queue: The queue for this direction + visited: Set of visited nodes for this direction + paths: Dictionary to store paths for this direction + other_visited: Set of visited nodes for the other direction + + Returns: + The intersection node if found, None otherwise + """ + if not queue: + return None + + current, path = queue.popleft() + + for neighbor in graph[current]: + if neighbor not in visited: + visited.add(neighbor) + new_path = path + [neighbor] + paths[neighbor] = new_path + queue.append((neighbor, new_path)) + + # Check if the neighbor is in the other visited set (intersection) + if neighbor in other_visited: + return neighbor + + return None + + +def construct_path( + intersection: int, forward_paths: Dict[int, List[int]], backward_paths: Dict[int, List[int]] +) -> List[int]: + """ + Construct the full path from the intersection point. + + Args: + intersection: The node where the two searches met + forward_paths: Paths from start to intersection + backward_paths: Paths from goal to intersection + + Returns: + The complete path from start to goal + """ + # Get the path from start to intersection + forward_path = forward_paths[intersection] + + # Get the path from goal to intersection and reverse it + backward_path = backward_paths[intersection] + backward_path.reverse() + + # Combine the paths (remove the duplicate intersection node) + return forward_path + backward_path[1:] + + +def main(): + """ + Example usage and test cases for bidirectional search + """ + # Example graph represented as an adjacency list + 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, start, goal) + print(f"Path from {start} to {goal}: {path}") + # Expected: Path from 0 to 11: [0, 1, 3, 7, 11] or similar valid shortest path + + # Test case 2: Start and goal are the same + start, goal = 5, 5 + path = bidirectional_search(graph, start, goal) + print(f"Path from {start} to {goal}: {path}") + # Expected: Path from 5 to 5: [5] + + # 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(disconnected_graph, start, goal) + print(f"Path from {start} to {goal}: {path}") + # Expected: Path from 0 to 3: None + + +if __name__ == "__main__": + main() \ No newline at end of file From 336452953ee51d4f02a7fa68a723bd0b0b97fe86 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 31 Mar 2025 17:34:27 +0000 Subject: [PATCH 02/12] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- graphs/bidirectional_search.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/graphs/bidirectional_search.py b/graphs/bidirectional_search.py index 7d175ec6eb3a..4bdecdc1fe3f 100644 --- a/graphs/bidirectional_search.py +++ b/graphs/bidirectional_search.py @@ -107,7 +107,9 @@ def expand_search( def construct_path( - intersection: int, forward_paths: Dict[int, List[int]], backward_paths: Dict[int, List[int]] + intersection: int, + forward_paths: Dict[int, List[int]], + backward_paths: Dict[int, List[int]], ) -> List[int]: """ Construct the full path from the intersection point. @@ -178,4 +180,4 @@ def main(): if __name__ == "__main__": - main() \ No newline at end of file + main() From f71b1674f6d75a6d5c9eef1a8a66693561346a08 Mon Sep 17 00:00:00 2001 From: prajwalc22 Date: Mon, 31 Mar 2025 23:09:30 +0530 Subject: [PATCH 03/12] Fix style and linting issues in bidirectional search --- graphs/bidirectional_search.py | 198 ++++++++++++++++----------------- 1 file changed, 95 insertions(+), 103 deletions(-) diff --git a/graphs/bidirectional_search.py b/graphs/bidirectional_search.py index 7d175ec6eb3a..e2327d545c71 100644 --- a/graphs/bidirectional_search.py +++ b/graphs/bidirectional_search.py @@ -1,25 +1,25 @@ """ -Bidirectional Search Algorithm +Bidirectional Search Algorithm. -A bidirectional search algorithm searches from both the source and the target -simultaneously, meeting somewhere in the middle. This can significantly reduce -the search space and improve performance compared to a single-direction search -in many scenarios. +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 -from typing import Dict, List, Optional, Set, Tuple +from typing import Dict, List, Optional def bidirectional_search( graph: Dict[int, List[int]], start: int, goal: int ) -> Optional[List[int]]: """ - Perform bidirectional search on a graph to find the shortest path - between start and goal nodes. + 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 @@ -28,6 +28,35 @@ def bidirectional_search( 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, 0, 11) + [0, 1, 3, 7, 11] + >>> bidirectional_search(graph, 5, 5) + [5] + >>> disconnected_graph = { + ... 0: [1, 2], + ... 1: [0], + ... 2: [0], + ... 3: [4], + ... 4: [3], + ... } + >>> bidirectional_search(disconnected_graph, 0, 3) is None + True """ if start == goal: return [start] @@ -36,107 +65,73 @@ def bidirectional_search( if start not in graph or goal not in graph: return None - # Initialize forward and backward search queues - forward_queue = deque([(start, [start])]) - backward_queue = deque([(goal, [goal])]) + # Initialize forward and backward search dictionaries + # Each maps a node to its parent in the search + forward_parents = {start: None} + backward_parents = {goal: None} - # Initialize visited sets for both directions - forward_visited: Set[int] = {start} - backward_visited: Set[int] = {goal} + # Initialize forward and backward search queues + forward_queue = deque([start]) + backward_queue = deque([goal]) - # Dictionary to store paths - forward_paths: Dict[int, List[int]] = {start: [start]} - backward_paths: Dict[int, List[int]] = {goal: [goal]} + # Intersection node (where the two searches meet) + intersection = None - while forward_queue and backward_queue: + # 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, forward_queue, forward_visited, forward_paths, backward_visited - ) - if intersection: - return construct_path(intersection, forward_paths, backward_paths) - - # Expand backward search - intersection = expand_search( - graph, backward_queue, backward_visited, backward_paths, forward_visited - ) - if intersection: - return construct_path(intersection, forward_paths, backward_paths) - - # No path found - return None - - -def expand_search( - graph: Dict[int, List[int]], - queue: deque, - visited: Set[int], - paths: Dict[int, List[int]], - other_visited: Set[int], -) -> Optional[int]: - """ - Expand the search in one direction and check for intersection. - - Args: - graph: The graph - queue: The queue for this direction - visited: Set of visited nodes for this direction - paths: Dictionary to store paths for this direction - other_visited: Set of visited nodes for the other direction - - Returns: - The intersection node if found, None otherwise - """ - if not queue: + if forward_queue: + current = forward_queue.popleft() + for neighbor in graph[current]: + if neighbor not in forward_parents: + forward_parents[neighbor] = current + forward_queue.append(neighbor) + + # Check if this creates an intersection + if neighbor in backward_parents: + intersection = neighbor + break + + # If no intersection found, expand backward search + if intersection is None and backward_queue: + current = backward_queue.popleft() + for neighbor in graph[current]: + if neighbor not in backward_parents: + backward_parents[neighbor] = current + backward_queue.append(neighbor) + + # Check if this creates an intersection + if neighbor in forward_parents: + intersection = neighbor + break + + # If no intersection found, there's no path + if intersection is None: return None - current, path = queue.popleft() - - for neighbor in graph[current]: - if neighbor not in visited: - visited.add(neighbor) - new_path = path + [neighbor] - paths[neighbor] = new_path - queue.append((neighbor, new_path)) + # Construct path from start to intersection + forward_path = [] + current = intersection + while current is not None: + forward_path.append(current) + current = forward_parents[current] + forward_path.reverse() - # Check if the neighbor is in the other visited set (intersection) - if neighbor in other_visited: - return neighbor + # Construct path from intersection to goal + backward_path = [] + current = backward_parents[intersection] + while current is not None: + backward_path.append(current) + current = backward_parents[current] - return None + # Return the complete path + return forward_path + backward_path -def construct_path( - intersection: int, forward_paths: Dict[int, List[int]], backward_paths: Dict[int, List[int]] -) -> List[int]: - """ - Construct the full path from the intersection point. - - Args: - intersection: The node where the two searches met - forward_paths: Paths from start to intersection - backward_paths: Paths from goal to intersection - - Returns: - The complete path from start to goal - """ - # Get the path from start to intersection - forward_path = forward_paths[intersection] - - # Get the path from goal to intersection and reverse it - backward_path = backward_paths[intersection] - backward_path.reverse() - - # Combine the paths (remove the duplicate intersection node) - return forward_path + backward_path[1:] - - -def main(): - """ - Example usage and test cases for bidirectional search - """ +def main() -> None: + """Run example of bidirectional search algorithm.""" # Example graph represented as an adjacency list - graph = { + example_graph = { 0: [1, 2], 1: [0, 3, 4], 2: [0, 5, 6], @@ -153,15 +148,13 @@ def main(): # Test case 1: Path exists start, goal = 0, 11 - path = bidirectional_search(graph, start, goal) + path = bidirectional_search(example_graph, start, goal) print(f"Path from {start} to {goal}: {path}") - # Expected: Path from 0 to 11: [0, 1, 3, 7, 11] or similar valid shortest path # Test case 2: Start and goal are the same start, goal = 5, 5 - path = bidirectional_search(graph, start, goal) + path = bidirectional_search(example_graph, start, goal) print(f"Path from {start} to {goal}: {path}") - # Expected: Path from 5 to 5: [5] # Test case 3: No path exists (disconnected graph) disconnected_graph = { @@ -174,7 +167,6 @@ def main(): start, goal = 0, 3 path = bidirectional_search(disconnected_graph, start, goal) print(f"Path from {start} to {goal}: {path}") - # Expected: Path from 0 to 3: None if __name__ == "__main__": From b39eaca70655b30a0ebf1d8b5e8c92016bd56d3d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 31 Mar 2025 17:44:52 +0000 Subject: [PATCH 04/12] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- graphs/bidirectional_search.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphs/bidirectional_search.py b/graphs/bidirectional_search.py index e2327d545c71..e487a993639c 100644 --- a/graphs/bidirectional_search.py +++ b/graphs/bidirectional_search.py @@ -170,4 +170,4 @@ def main() -> None: if __name__ == "__main__": - main() \ No newline at end of file + main() From 50ce48a990958fd346930cdfab6ac49695b78213 Mon Sep 17 00:00:00 2001 From: prajwalc22 Date: Mon, 31 Mar 2025 23:22:35 +0530 Subject: [PATCH 05/12] Add doctest for main function --- graphs/bidirectional_search.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/graphs/bidirectional_search.py b/graphs/bidirectional_search.py index e2327d545c71..93634b1ef960 100644 --- a/graphs/bidirectional_search.py +++ b/graphs/bidirectional_search.py @@ -129,7 +129,15 @@ def bidirectional_search( def main() -> None: - """Run example of bidirectional search algorithm.""" + """ + 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], From 334fa0f40b930685e905dbc1806a59f01c8271af Mon Sep 17 00:00:00 2001 From: prajwalc22 Date: Mon, 31 Mar 2025 23:24:59 +0530 Subject: [PATCH 06/12] Add doctest for main function --- graphs/bidirectional_search.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphs/bidirectional_search.py b/graphs/bidirectional_search.py index 80725baa6445..93634b1ef960 100644 --- a/graphs/bidirectional_search.py +++ b/graphs/bidirectional_search.py @@ -178,4 +178,4 @@ def main() -> None: if __name__ == "__main__": - main() + main() \ No newline at end of file From 7ab5f233a55ca95328a84e188ba2f686a6dc67c9 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 31 Mar 2025 17:55:36 +0000 Subject: [PATCH 07/12] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- graphs/bidirectional_search.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/graphs/bidirectional_search.py b/graphs/bidirectional_search.py index 93634b1ef960..4ffde21ac2b4 100644 --- a/graphs/bidirectional_search.py +++ b/graphs/bidirectional_search.py @@ -131,7 +131,7 @@ def bidirectional_search( def main() -> None: """ Run example of bidirectional search algorithm. - + Examples: >>> main() # doctest: +NORMALIZE_WHITESPACE Path from 0 to 11: [0, 1, 3, 7, 11] @@ -178,4 +178,4 @@ def main() -> None: if __name__ == "__main__": - main() \ No newline at end of file + main() From 9aee6164d75ebe24071986fcd44f0d697b6e35e4 Mon Sep 17 00:00:00 2001 From: prajwalc22 Date: Mon, 31 Mar 2025 23:39:58 +0530 Subject: [PATCH 08/12] fixed deprications --- graphs/bidirectional_search.py | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/graphs/bidirectional_search.py b/graphs/bidirectional_search.py index 93634b1ef960..81d5b1a01db4 100644 --- a/graphs/bidirectional_search.py +++ b/graphs/bidirectional_search.py @@ -12,12 +12,12 @@ """ from collections import deque -from typing import Dict, List, Optional +from typing import Optional def bidirectional_search( - graph: Dict[int, List[int]], start: int, goal: int -) -> Optional[List[int]]: + graph: dict[int, list[int]], start: int, goal: int +) -> list[int] | None: """ Perform bidirectional search on a graph to find the shortest path. @@ -67,8 +67,8 @@ def bidirectional_search( # Initialize forward and backward search dictionaries # Each maps a node to its parent in the search - forward_parents = {start: None} - backward_parents = {goal: None} + 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]) @@ -110,19 +110,19 @@ def bidirectional_search( return None # Construct path from start to intersection - forward_path = [] - current = intersection - while current is not None: - forward_path.append(current) - current = forward_parents[current] + forward_path: list[int] = [] + current_forward: int | None = intersection + while current_forward is not None: + forward_path.append(current_forward) + current_forward = forward_parents[current_forward] forward_path.reverse() # Construct path from intersection to goal - backward_path = [] - current = backward_parents[intersection] - while current is not None: - backward_path.append(current) - current = backward_parents[current] + backward_path: list[int] = [] + current_backward: int | None = backward_parents[intersection] + while current_backward is not None: + backward_path.append(current_backward) + current_backward = backward_parents[current_backward] # Return the complete path return forward_path + backward_path @@ -131,7 +131,7 @@ def bidirectional_search( def main() -> None: """ Run example of bidirectional search algorithm. - + Examples: >>> main() # doctest: +NORMALIZE_WHITESPACE Path from 0 to 11: [0, 1, 3, 7, 11] From 16de735a24ced2c44301d6025ae0a25e2208ce96 Mon Sep 17 00:00:00 2001 From: prajwalc22 Date: Mon, 31 Mar 2025 23:40:55 +0530 Subject: [PATCH 09/12] fixed deprications --- graphs/bidirectional_search.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphs/bidirectional_search.py b/graphs/bidirectional_search.py index 2273dc59a83c..81d5b1a01db4 100644 --- a/graphs/bidirectional_search.py +++ b/graphs/bidirectional_search.py @@ -178,4 +178,4 @@ def main() -> None: if __name__ == "__main__": - main() + main() \ No newline at end of file From d4f29186da5d096b306ce94d9ba97a8f51ea674c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 31 Mar 2025 18:11:27 +0000 Subject: [PATCH 10/12] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- graphs/bidirectional_search.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphs/bidirectional_search.py b/graphs/bidirectional_search.py index 81d5b1a01db4..2273dc59a83c 100644 --- a/graphs/bidirectional_search.py +++ b/graphs/bidirectional_search.py @@ -178,4 +178,4 @@ def main() -> None: if __name__ == "__main__": - main() \ No newline at end of file + main() From 7a0dc1690a75d04c6ec925c9b70f772ea3bc47cb Mon Sep 17 00:00:00 2001 From: prajwalc22 Date: Mon, 31 Mar 2025 23:48:02 +0530 Subject: [PATCH 11/12] removed unused import --- graphs/bidirectional_search.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphs/bidirectional_search.py b/graphs/bidirectional_search.py index 81d5b1a01db4..6866e76346b5 100644 --- a/graphs/bidirectional_search.py +++ b/graphs/bidirectional_search.py @@ -12,7 +12,7 @@ """ from collections import deque -from typing import Optional + def bidirectional_search( From 8b309e62805b318966557a07c8677d6ee550d0f1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 31 Mar 2025 18:19:13 +0000 Subject: [PATCH 12/12] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- graphs/bidirectional_search.py | 1 - 1 file changed, 1 deletion(-) diff --git a/graphs/bidirectional_search.py b/graphs/bidirectional_search.py index a8acfe8fb630..0c609e5509f7 100644 --- a/graphs/bidirectional_search.py +++ b/graphs/bidirectional_search.py @@ -14,7 +14,6 @@ from collections import deque - def bidirectional_search( graph: dict[int, list[int]], start: int, goal: int ) -> list[int] | None: