diff --git a/pydatastructs/graphs/algorithms.py b/pydatastructs/graphs/algorithms.py index ea3322c02..fd5249477 100644 --- a/pydatastructs/graphs/algorithms.py +++ b/pydatastructs/graphs/algorithms.py @@ -23,7 +23,7 @@ 'all_pair_shortest_paths', 'topological_sort', 'topological_sort_parallel', - 'max_flow' + 'max_flow', ] Stack = Queue = deque @@ -700,12 +700,14 @@ def shortest_paths(graph: Graph, algorithm: str, 'bellman_ford' -> Bellman-Ford algorithm as given in [1]. 'dijkstra' -> Dijkstra algorithm as given in [2]. + + 'A_star' -> A* algorithm as given in [3]. source: str The name of the source the node. target: str The name of the target node. Optional, by default, all pair shortest paths - are returned. + are returned. Required for A* algorithm. backend: pydatastructs.Backend The backend to be used. Optional, by default, the best available @@ -736,17 +738,28 @@ def shortest_paths(graph: Graph, algorithm: str, ({'V1': 0, 'V2': 11, 'V3': 21}, {'V1': None, 'V2': 'V1', 'V3': 'V2'}) >>> shortest_paths(G, 'dijkstra', 'V1') ({'V2': 11, 'V3': 21, 'V1': 0}, {'V1': None, 'V2': 'V1', 'V3': 'V2'}) - + >>> start = AdjacencyListGraphNode("0,0") + >>> middle = AdjacencyListGraphNode("1,1") + >>> goal = AdjacencyListGraphNode("2,2") + >>> G2 = Graph(start, middle, goal) + >>> G2.add_edge('0,0', '1,1', 2) + >>> G2.add_edge('1,1', '2,2', 2) + >>> dist, pred = shortest_paths(G2, 'a_star', '0,0', '2,2') + >>> dist + 4 + >>> pred == {'0,0': None, '1,1': '0,0', '2,2': '1,1'} + True References ========== .. [1] https://en.wikipedia.org/wiki/Bellman%E2%80%93Ford_algorithm .. [2] https://en.wikipedia.org/wiki/Dijkstra%27s_algorithm + .. [3] https://en.wikipedia.org/wiki/A*_search_algorithm """ raise_if_backend_is_not_python( shortest_paths, kwargs.get('backend', Backend.PYTHON)) import pydatastructs.graphs.algorithms as algorithms - func = "_" + algorithm + "_" + graph._impl + func = "_" + algorithm.lower() + "_" + graph._impl if not hasattr(algorithms, func): raise NotImplementedError( "Currently %s algorithm isn't implemented for " @@ -811,6 +824,67 @@ def _dijkstra_adjacency_list(graph: Graph, start: str, target: str): _dijkstra_adjacency_matrix = _dijkstra_adjacency_list +def _a_star_adjacency_list(graph: Graph, source: str, target: str) -> tuple: + """ + A* pathfinding algorithm implementation similar to Dijkstra's structure. + Parameters + ========== + graph: Graph + The graph to search through + source: str + Starting node name + target: str + Target node name + Returns + ======= + (distance, predecessors): tuple + Distance to target and dictionary of predecessors + """ + def heuristic(node: str, goal: str) -> float: + """Manhattan distance heuristic for A*""" + try: + x1, y1 = map(int, node.split(',')) + x2, y2 = map(int, goal.split(',')) + return abs(x1 - x2) + abs(y1 - y2) + except ValueError: + raise ValueError(f"Invalid node format: {node}. Expected 'x,y'.") + if source not in graph.vertices or target not in graph.vertices: + raise KeyError(f"Either source '{source}' or target '{target}' is not in the graph.") + visited = {v: False for v in graph.vertices} + dist = {v: float('inf') for v in graph.vertices} + pred = {v: None for v in graph.vertices} + dist[source] = 0 + from pydatastructs.miscellaneous_data_structures.queue import PriorityQueue, BinomialHeapPriorityQueue + pq = PriorityQueue(implementation='binomial_heap') + f_score = heuristic(source, target) + pq.push(source, f_score) + while not pq.is_empty: + current = pq.pop() + if current == target: + result = (dist[target], dict(sorted(pred.items()))) + return result + if visited[current]: + continue + visited[current] = True + neighbors = graph.neighbors(current) + if not neighbors: + continue + for neighbor in neighbors: + if visited[neighbor.name]: + continue + edge = graph.get_edge(current, neighbor.name) + if not edge: + continue + new_dist = dist[current] + edge.value + if new_dist < dist[neighbor.name]: + dist[neighbor.name] = new_dist + pred[neighbor.name] = current + f_score = new_dist + heuristic(neighbor.name, target) + pq.push(neighbor.name, f_score) + if dist[target] == float('inf'): + raise ValueError(f"Either source '{source}' and target '{target}' have no path between them.") + return float('inf'), dict(sorted(pred.items())) +_a_star_adjacency_matrix = _a_star_adjacency_list def all_pair_shortest_paths(graph: Graph, algorithm: str, **kwargs) -> tuple: """ diff --git a/pydatastructs/graphs/tests/test_algorithms.py b/pydatastructs/graphs/tests/test_algorithms.py index fde3571da..a8b15e9da 100644 --- a/pydatastructs/graphs/tests/test_algorithms.py +++ b/pydatastructs/graphs/tests/test_algorithms.py @@ -1,3 +1,4 @@ +import pytest from pydatastructs import (breadth_first_search, Graph, breadth_first_search_parallel, minimum_spanning_tree, minimum_spanning_tree_parallel, strongly_connected_components, @@ -314,13 +315,56 @@ def _test_shortest_paths_negative_edges(ds, algorithm): dist, pred = shortest_paths(graph, algorithm, 's', 'd') assert dist == 2 assert pred == {'s': None, 'a': 'b', 'b': 's', 'c': 'a', 'd': 'c'} - + def _test_a_star_manhattan(ds): + import pydatastructs.utils.misc_util as utils + GraphNode = getattr(utils, "Adjacency" + ds + "GraphNode") + vertices = [ + GraphNode("0,0"), + GraphNode("1,1"), + GraphNode("2,2") + ] + graph = Graph(*vertices) + graph.add_edge("0,0", "1,1", 2) + graph.add_edge("1,1", "2,2", 2) + distance, pred = shortest_paths(graph, 'a_star', "0,0", "2,2") + assert distance == 4 + assert pred == {'0,0': None, '1,1': '0,0', '2,2': '1,1'} + no_path_graph = Graph( + GraphNode("0,0"), + GraphNode("1,1"), + GraphNode("2,2") + ) + with pytest.raises(ValueError, match="Either source '0,0' and target '2,2' have no path between them."): + shortest_paths(no_path_graph, 'a_star', "0,0", "2,2") + same_node_graph = Graph(GraphNode("1,1")) + distance, pred = shortest_paths(same_node_graph, 'a_star', "1,1", "1,1") + assert distance == 0 + assert pred == {'1,1': None} + invalid_graph = Graph(GraphNode("invalid")) + with pytest.raises(ValueError, match="Invalid node format: invalid. Expected 'x,y'."): + shortest_paths(invalid_graph, 'a_star', "invalid", "invalid") + complex_vertices = [ + GraphNode("0,0"), + GraphNode("0,1"), + GraphNode("1,0"), + GraphNode("1,1") + ] + complex_graph = Graph(*complex_vertices) + complex_graph.add_edge("0,0", "0,1", 1) + complex_graph.add_edge("0,1", "1,1", 1) + complex_graph.add_edge("0,0", "1,0", 2) + complex_graph.add_edge("1,0", "1,1", 1) + distance, pred = shortest_paths(complex_graph, 'a_star', "0,0", "1,1") + assert distance == 2 + assert pred == {'0,0': None, '0,1': '0,0', '1,1': '0,1', '1,0': '0,0'} _test_shortest_paths_positive_edges("List", 'bellman_ford') _test_shortest_paths_positive_edges("Matrix", 'bellman_ford') _test_shortest_paths_negative_edges("List", 'bellman_ford') _test_shortest_paths_negative_edges("Matrix", 'bellman_ford') _test_shortest_paths_positive_edges("List", 'dijkstra') _test_shortest_paths_positive_edges("Matrix", 'dijkstra') + _test_a_star_manhattan("List") + _test_a_star_manhattan("Matrix") def test_all_pair_shortest_paths(): diff --git a/pydatastructs/linear_data_structures/_backend/cpp/_algorithms.cp312-win_amd64.pyd b/pydatastructs/linear_data_structures/_backend/cpp/_algorithms.cp312-win_amd64.pyd new file mode 100644 index 000000000..c66d0431b Binary files /dev/null and b/pydatastructs/linear_data_structures/_backend/cpp/_algorithms.cp312-win_amd64.pyd differ diff --git a/pydatastructs/linear_data_structures/_backend/cpp/_arrays.cp312-win_amd64.pyd b/pydatastructs/linear_data_structures/_backend/cpp/_arrays.cp312-win_amd64.pyd new file mode 100644 index 000000000..88bc269dc Binary files /dev/null and b/pydatastructs/linear_data_structures/_backend/cpp/_arrays.cp312-win_amd64.pyd differ diff --git a/pydatastructs/miscellaneous_data_structures/_backend/cpp/_stack.cp312-win_amd64.pyd b/pydatastructs/miscellaneous_data_structures/_backend/cpp/_stack.cp312-win_amd64.pyd new file mode 100644 index 000000000..892a45886 Binary files /dev/null and b/pydatastructs/miscellaneous_data_structures/_backend/cpp/_stack.cp312-win_amd64.pyd differ diff --git a/pydatastructs/trees/_backend/cpp/_trees.cp312-win_amd64.pyd b/pydatastructs/trees/_backend/cpp/_trees.cp312-win_amd64.pyd new file mode 100644 index 000000000..9aa2aa293 Binary files /dev/null and b/pydatastructs/trees/_backend/cpp/_trees.cp312-win_amd64.pyd differ diff --git a/pydatastructs/utils/_backend/cpp/_nodes.cp312-win_amd64.pyd b/pydatastructs/utils/_backend/cpp/_nodes.cp312-win_amd64.pyd new file mode 100644 index 000000000..053b9e88a Binary files /dev/null and b/pydatastructs/utils/_backend/cpp/_nodes.cp312-win_amd64.pyd differ