diff --git a/pydatastructs/graphs/algorithms.py b/pydatastructs/graphs/algorithms.py index ea3322c02..5f5963ab9 100644 --- a/pydatastructs/graphs/algorithms.py +++ b/pydatastructs/graphs/algorithms.py @@ -2,6 +2,7 @@ Contains algorithms associated with graph data structure. """ +import pytest from collections import deque from concurrent.futures import ThreadPoolExecutor from pydatastructs.utils.misc_util import ( @@ -700,6 +701,8 @@ 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 @@ -736,12 +739,24 @@ 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)) @@ -811,6 +826,86 @@ 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: + return dist[target], dict(sorted(pred.items())) + + 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..52078d431 100644 --- a/pydatastructs/graphs/tests/test_algorithms.py +++ b/pydatastructs/graphs/tests/test_algorithms.py @@ -314,13 +314,71 @@ 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): + """ + Test suite for the A* algorithm using the Manhattan distance heuristic. + """ + GraphNode = getattr(utils, "Adjacency" + ds + "GraphNode") + + # Case 1: Simple Path Test + 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'} + + # Case 2: No Path Between Nodes + 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") + + # Case 3: Same Source and Target Node + 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} + + # Case 4: Invalid Node Format + 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") + + # Case 5: Complex Graph with Multiple Paths + 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():