Skip to content

Implement Johnson's algorithm with tests; update Bellman-Ford and Dij… #12884

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 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
Implement Johnson's algorithm with tests; update Bellman-Ford and Dij…
…kstra algorithms
  • Loading branch information
Zakaria-Fakhri committed Aug 7, 2025
commit 8066d8f2be846a8e158d96f5c99bfa48c82701f6
243 changes: 177 additions & 66 deletions graphs/bellman_ford.py
Original file line number Diff line number Diff line change
@@ -1,73 +1,184 @@
from __future__ import annotations
"""
Bellman-Ford Algorithm implementation for single-source shortest path.

The Bellman-Ford algorithm can handle negative edge weights and detect negative cycles.
It has O(VE) time complexity where V is the number of vertices and E is the number of edges.

Check failure on line 5 in graphs/bellman_ford.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (E501)

graphs/bellman_ford.py:5:89: E501 Line too long (92 > 88)

def print_distance(distance: list[float], src):
print(f"Vertex\tShortest Distance from vertex {src}")
for i, d in enumerate(distance):
print(f"{i}\t\t{d}")
Author: Zakariae Fakhri
Date: August 2025
"""

from typing import Dict, Any, Optional, List, Tuple

Check failure on line 11 in graphs/bellman_ford.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (UP035)

graphs/bellman_ford.py:11:1: UP035 `typing.Tuple` is deprecated, use `tuple` instead

Check failure on line 11 in graphs/bellman_ford.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (UP035)

graphs/bellman_ford.py:11:1: UP035 `typing.List` is deprecated, use `list` instead

Check failure on line 11 in graphs/bellman_ford.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (UP035)

graphs/bellman_ford.py:11:1: UP035 `typing.Dict` is deprecated, use `dict` instead
from collections import defaultdict

Check failure on line 12 in graphs/bellman_ford.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (I001)

graphs/bellman_ford.py:11:1: I001 Import block is un-sorted or un-formatted

def check_negative_cycle(
graph: list[dict[str, int]], distance: list[float], edge_count: int
):
for j in range(edge_count):
u, v, w = (graph[j][k] for k in ["src", "dst", "weight"])
if distance[u] != float("inf") and distance[u] + w < distance[v]:
return True
return False


def bellman_ford(
graph: list[dict[str, int]], vertex_count: int, edge_count: int, src: int
) -> list[float]:
class Graph:
"""
Returns shortest paths from a vertex src to all
other vertices.
>>> edges = [(2, 1, -10), (3, 2, 3), (0, 3, 5), (0, 1, 4)]
>>> g = [{"src": s, "dst": d, "weight": w} for s, d, w in edges]
>>> bellman_ford(g, 4, 4, 0)
[0.0, -2.0, 8.0, 5.0]
>>> g = [{"src": s, "dst": d, "weight": w} for s, d, w in edges + [(1, 3, 5)]]
>>> bellman_ford(g, 4, 5, 0)
Traceback (most recent call last):
...
Exception: Negative cycle found
A lightweight graph class for algorithms that don't have access to the main Graph class.

Check failure on line 17 in graphs/bellman_ford.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (E501)

graphs/bellman_ford.py:17:89: E501 Line too long (92 > 88)
"""
distance = [float("inf")] * vertex_count
distance[src] = 0.0

for _ in range(vertex_count - 1):
for j in range(edge_count):
u, v, w = (graph[j][k] for k in ["src", "dst", "weight"])

if distance[u] != float("inf") and distance[u] + w < distance[v]:
distance[v] = distance[u] + w

negative_cycle_exists = check_negative_cycle(graph, distance, edge_count)
if negative_cycle_exists:
raise Exception("Negative cycle found")

return distance


if __name__ == "__main__":
import doctest

doctest.testmod()

V = int(input("Enter number of vertices: ").strip())
E = int(input("Enter number of edges: ").strip())

graph: list[dict[str, int]] = [{} for _ in range(E)]

for i in range(E):
print("Edge ", i + 1)
src, dest, weight = (
int(x)
for x in input("Enter source, destination, weight: ").strip().split(" ")
)
graph[i] = {"src": src, "dst": dest, "weight": weight}

source = int(input("\nEnter shortest path source:").strip())
shortest_distance = bellman_ford(graph, V, E, source)
print_distance(shortest_distance, 0)

Check failure on line 19 in graphs/bellman_ford.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (W293)

graphs/bellman_ford.py:19:1: W293 Blank line contains whitespace
def __init__(self):
self.adjacency_list = defaultdict(list)
self.vertices = set()

Check failure on line 23 in graphs/bellman_ford.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (W293)

graphs/bellman_ford.py:23:1: W293 Blank line contains whitespace
def add_edge(self, source: Any, destination: Any, weight: float) -> None:
self.adjacency_list[source].append((destination, weight))
self.vertices.add(source)
self.vertices.add(destination)

Check failure on line 28 in graphs/bellman_ford.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (W293)

graphs/bellman_ford.py:28:1: W293 Blank line contains whitespace
def get_neighbors(self, vertex: Any) -> List[Tuple[Any, float]]:

Check failure on line 29 in graphs/bellman_ford.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (UP006)

graphs/bellman_ford.py:29:45: UP006 Use `list` instead of `List` for type annotation
return self.adjacency_list.get(vertex, [])

def get_vertices(self) -> set:
return self.vertices.copy()

def has_vertex(self, vertex: Any) -> bool:
return vertex in self.vertices

def get_vertex_count(self) -> int:
return len(self.vertices)


class BellmanFord:
"""
Bellman-Ford algorithm implementation for single-source shortest path.

This algorithm can handle graphs with negative edge weights and can detect
negative cycles. It's particularly useful as a preprocessing step in
Johnson's algorithm.
"""

def __init__(self, graph):
"""
Initialize the Bellman-Ford algorithm with a graph.

Args:
graph: The weighted directed graph to process
"""
self.graph = graph
self.distances = {}
self.predecessors = {}

def find_shortest_paths(self, start_vertex: Any) -> Optional[Dict[Any, float]]:
"""
Find shortest paths from start_vertex to all other vertices.

Args:
start_vertex: The source vertex to start from

Returns:
Dictionary of vertex -> shortest distance, or None if negative cycle exists
"""
if not self.graph.has_vertex(start_vertex):
raise ValueError(f"Start vertex {start_vertex} not found in graph")

# Initialize distances
self.distances = {vertex: float('inf') for vertex in self.graph.get_vertices()}
self.distances[start_vertex] = 0
self.predecessors = {vertex: None for vertex in self.graph.get_vertices()}

# Relax edges V-1 times
vertex_count = self.graph.get_vertex_count()

for iteration in range(vertex_count - 1):
updated = False

for vertex in self.graph.get_vertices():
if self.distances[vertex] != float('inf'):
for neighbor, weight in self.graph.get_neighbors(vertex):
new_distance = self.distances[vertex] + weight

if new_distance < self.distances[neighbor]:
self.distances[neighbor] = new_distance
self.predecessors[neighbor] = vertex
updated = True

# Early termination if no updates in this iteration
if not updated:
break

# Check for negative cycles
if self._has_negative_cycle():
return None

return self.distances.copy()

def _has_negative_cycle(self) -> bool:
"""
Check if the graph contains a negative cycle.

Returns:
True if negative cycle exists, False otherwise
"""
for vertex in self.graph.get_vertices():
if self.distances[vertex] != float('inf'):
for neighbor, weight in self.graph.get_neighbors(vertex):
if self.distances[vertex] + weight < self.distances[neighbor]:
return True
return False

def get_path(self, start_vertex: Any, end_vertex: Any) -> Optional[List[Any]]:
"""
Get the shortest path from start_vertex to end_vertex.

Args:
start_vertex: Source vertex
end_vertex: Destination vertex

Returns:
List of vertices representing the path, or None if no path exists
"""
if not self.distances or end_vertex not in self.distances:
return None

if self.distances[end_vertex] == float('inf'):
return None

path = []
current = end_vertex

while current is not None:
path.append(current)
current = self.predecessors.get(current)

path.reverse()

# Verify the path starts with start_vertex
if path[0] != start_vertex:
return None

return path

def get_distance(self, vertex: Any) -> float:
"""
Get the shortest distance to a specific vertex.

Args:
vertex: The target vertex

Returns:
Shortest distance to the vertex
"""
return self.distances.get(vertex, float('inf'))

@staticmethod
def detect_negative_cycle(graph) -> bool:
"""
Static method to detect if a graph contains a negative cycle.

Args:
graph: The graph to check

Returns:
True if negative cycle exists, False otherwise
"""
vertices = graph.get_vertices()
if not vertices:
return False

# Pick any vertex as start
start_vertex = next(iter(vertices))
bf = BellmanFord(graph)
result = bf.find_shortest_paths(start_vertex)

return result is None
Loading