Skip to content

feat: Enhanced Functionality Introducing Graph Algorithms #606

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

Closed
wants to merge 1 commit into from
Closed
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
95 changes: 95 additions & 0 deletions pydatastructs/graphs/algorithms.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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:
"""
Expand Down
58 changes: 58 additions & 0 deletions pydatastructs/graphs/tests/test_algorithms.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():

Expand Down