Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

A_STAR algorithem Implementation #582

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 78 additions & 4 deletions pydatastructs/graphs/algorithms.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
'all_pair_shortest_paths',
'topological_sort',
'topological_sort_parallel',
'max_flow'
'max_flow',
]

Stack = Queue = deque
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 "
Expand Down Expand Up @@ -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:
"""
Expand Down
46 changes: 45 additions & 1 deletion pydatastructs/graphs/tests/test_algorithms.py
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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():

Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading