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 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
221 changes: 166 additions & 55 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"])
def __init__(self):
self.adjacency_list = defaultdict(list)
self.vertices = set()

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)

if distance[u] != float("inf") and distance[u] + w < distance[v]:
distance[v] = distance[u] + w
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:50: UP006 Use `tuple` instead of `Tuple` for type annotation

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, [])

negative_cycle_exists = check_negative_cycle(graph, distance, edge_count)
if negative_cycle_exists:
raise Exception("Negative cycle found")
def get_vertices(self) -> set:
return self.vertices.copy()

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

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

if __name__ == "__main__":
import doctest

doctest.testmod()
class BellmanFord:
"""
Bellman-Ford algorithm implementation for single-source shortest path.

V = int(input("Enter number of vertices: ").strip())
E = int(input("Enter number of edges: ").strip())
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.
"""

graph: list[dict[str, int]] = [{} for _ in range(E)]
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]]:

Check failure on line 62 in graphs/bellman_ford.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (UP006)

graphs/bellman_ford.py:62:66: UP006 Use `dict` instead of `Dict` for type annotation

Check failure on line 62 in graphs/bellman_ford.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (UP045)

graphs/bellman_ford.py:62:57: UP045 Use `X | None` for type annotations
"""
Find shortest paths from start_vertex to all other vertices.

Args:
start_vertex: The source vertex to start from

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}
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

source = int(input("\nEnter shortest path source:").strip())
shortest_distance = bellman_ford(graph, V, E, source)
print_distance(shortest_distance, 0)
# 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