Skip to content

feat: Implement Yen's Algorithm for K-Shortest Paths #625

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

Open
wants to merge 3 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
2 changes: 2 additions & 0 deletions docs/source/pydatastructs/graphs/algorithms.rst
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,5 @@ Algorithms
.. autofunction:: pydatastructs.topological_sort

.. autofunction:: pydatastructs.topological_sort_parallel

.. autofunction:: pydatastructs.yen_algorithm
3 changes: 2 additions & 1 deletion pydatastructs/graphs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
all_pair_shortest_paths,
topological_sort,
topological_sort_parallel,
max_flow
max_flow,
yen_algorithm
)

__all__.extend(algorithms.__all__)
116 changes: 115 additions & 1 deletion pydatastructs/graphs/algorithms.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@
'all_pair_shortest_paths',
'topological_sort',
'topological_sort_parallel',
'max_flow'
'max_flow',
'yen_algorithm'
]

Stack = Queue = deque
Expand Down Expand Up @@ -1216,3 +1217,116 @@ def max_flow(graph, source, sink, algorithm='edmonds_karp', **kwargs):
f"Currently {algorithm} algorithm isn't implemented for "
"performing max flow on graphs.")
return getattr(algorithms, func)(graph, source, sink)

def yen_algorithm(graph, source, target, K, **kwargs):
"""
Finds the K shortest paths from source to target in a graph using Yen's algorithm.

Parameters
==========
graph: Graph
The graph on which Yen's algorithm is to be performed.
source: str
The name of the source node.
target: str
The name of the target node.
K: int
The number of shortest paths to find.
backend: pydatastructs.Backend
The backend to be used.
Optional, by default, the best available backend is used.

Returns
=======
list
A list of the K shortest paths, where each path is a list of node names.
"""
raise_if_backend_is_not_python(
yen_algorithm, kwargs.get('backend', Backend.PYTHON))

def dijkstra_shortest_path(graph, source, target):
"""
Helper function to find the shortest path using Dijkstra's algorithm.
"""
dist, pred = {}, {}
for v in graph.vertices:
dist[v] = float('inf')
pred[v] = None
dist[source] = 0
pq = PriorityQueue(implementation='binomial_heap')
for vertex in dist:
pq.push(vertex, dist[vertex])
while not pq.is_empty:
u = pq.pop()
if u == target:
break
for v in graph.neighbors(u):
edge_str = u + '_' + v.name
if edge_str in graph.edge_weights:
alt = dist[u] + graph.edge_weights[edge_str].value
if alt < dist[v.name]:
dist[v.name] = alt
pred[v.name] = u
pq.push(v.name, alt)
path = []
if dist[target] != float('inf'):
current = target
while current is not None:
path.append(current)
current = pred[current]
path.reverse()
return path

A = []
B = []

shortest_path = dijkstra_shortest_path(graph, source, target)
if not shortest_path:
return A
A.append(shortest_path)

for k in range(1, K):
for i in range(len(A[k-1]) - 1):
spur_node = A[k-1][i]
root_path = A[k-1][:i+1]

edges_removed = []
for path in A:
if len(path) > i and root_path == path[:i+1]:
u = path[i]
v = path[i+1]
edge_str = u + '_' + v
if edge_str in graph.edge_weights:
edges_removed.append((u, v, graph.edge_weights[edge_str].value))
graph.remove_edge(u, v)

nodes_removed = []
for node_name in root_path[:-1]:
if node_name != spur_node:
node = graph.__getattribute__(node_name)
nodes_removed.append(node)
graph.remove_vertex(node_name)

spur_path = dijkstra_shortest_path(graph, spur_node, target)

if spur_path:
total_path = root_path[:-1] + spur_path
total_cost = sum(graph.edge_weights[total_path[i] + '_' + total_path[i+1]].value
for i in range(len(total_path)-1))
B.append((total_cost, total_path))

for u, v, w in edges_removed:
graph.add_edge(u, v, w)
for node in nodes_removed:
graph.add_vertex(node)

if not B:
break

B.sort(key=lambda x: x[0])

shortest_candidate = B.pop(0)[1]
if shortest_candidate not in A:
A.append(shortest_candidate)

return A
39 changes: 38 additions & 1 deletion pydatastructs/graphs/tests/test_algorithms.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
breadth_first_search_parallel, minimum_spanning_tree,
minimum_spanning_tree_parallel, strongly_connected_components,
depth_first_search, shortest_paths,all_pair_shortest_paths, topological_sort,
topological_sort_parallel, max_flow)
topological_sort_parallel, max_flow, yen_algorithm)
from pydatastructs.utils.raises_util import raises

def test_breadth_first_search():
Expand Down Expand Up @@ -448,3 +448,40 @@ def _test_max_flow(ds, algorithm):
_test_max_flow("Matrix", "edmonds_karp")
_test_max_flow("List", "dinic")
_test_max_flow("Matrix", "dinic")

def test_yen_algorithm():
"""
Test function for Yen's Algorithm to find K shortest paths.
"""
def _test_yen_algorithm(ds):
import pydatastructs.utils.misc_util as utils
GraphNode = getattr(utils, "Adjacency" + ds + "GraphNode")

V1 = GraphNode("V1")
V2 = GraphNode("V2")
V3 = GraphNode("V3")
V4 = GraphNode("V4")
V5 = GraphNode("V5")

G = Graph(V1, V2, V3, V4, V5)

G.add_edge(V1.name, V2.name, 1)
G.add_edge(V2.name, V3.name, 2)
G.add_edge(V3.name, V4.name, 1)
G.add_edge(V4.name, V5.name, 3)
G.add_edge(V1.name, V3.name, 4)
G.add_edge(V3.name, V5.name, 2)

k_shortest_paths = yen_algorithm(G, V1.name, V5.name, K=3)

expected_paths = [
['V1', 'V2', 'V3', 'V5'],
['V1', 'V3', 'V5'],
['V1', 'V2', 'V3', 'V4', 'V5']
]

assert len(k_shortest_paths) == 3, "Expected 3 shortest paths"
for i, path in enumerate(k_shortest_paths):
assert path == expected_paths[i], f"Path {i} does not match expected path. Got {path}, expected {expected_paths[i]}"

_test_yen_algorithm("List")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why don't we support matrix representation of graphs?

Copy link
Author

@asmit27rai asmit27rai Mar 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why don't we support matrix representation of graphs?

The AdjacencyMatrix implementation does not support dynamic modifications like removing vertices or edges during runtime. Since Yen's algorithm requires modifying the graph (e.g., removing edges and vertices to find alternative paths), it cannot work with the AdjacencyMatrix implementation in its current form.

Loading