Skip to content

Commit 8066d8f

Browse files
Implement Johnson's algorithm with tests; update Bellman-Ford and Dijkstra algorithms
1 parent 7a0fee4 commit 8066d8f

File tree

4 files changed

+1182
-175
lines changed

4 files changed

+1182
-175
lines changed

graphs/bellman_ford.py

Lines changed: 177 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,73 +1,184 @@
1-
from __future__ import annotations
1+
"""
2+
Bellman-Ford Algorithm implementation for single-source shortest path.
23
4+
The Bellman-Ford algorithm can handle negative edge weights and detect negative cycles.
5+
It has O(VE) time complexity where V is the number of vertices and E is the number of edges.
36
4-
def print_distance(distance: list[float], src):
5-
print(f"Vertex\tShortest Distance from vertex {src}")
6-
for i, d in enumerate(distance):
7-
print(f"{i}\t\t{d}")
7+
Author: Zakariae Fakhri
8+
Date: August 2025
9+
"""
810

11+
from typing import Dict, Any, Optional, List, Tuple
12+
from collections import defaultdict
913

10-
def check_negative_cycle(
11-
graph: list[dict[str, int]], distance: list[float], edge_count: int
12-
):
13-
for j in range(edge_count):
14-
u, v, w = (graph[j][k] for k in ["src", "dst", "weight"])
15-
if distance[u] != float("inf") and distance[u] + w < distance[v]:
16-
return True
17-
return False
1814

19-
20-
def bellman_ford(
21-
graph: list[dict[str, int]], vertex_count: int, edge_count: int, src: int
22-
) -> list[float]:
15+
class Graph:
2316
"""
24-
Returns shortest paths from a vertex src to all
25-
other vertices.
26-
>>> edges = [(2, 1, -10), (3, 2, 3), (0, 3, 5), (0, 1, 4)]
27-
>>> g = [{"src": s, "dst": d, "weight": w} for s, d, w in edges]
28-
>>> bellman_ford(g, 4, 4, 0)
29-
[0.0, -2.0, 8.0, 5.0]
30-
>>> g = [{"src": s, "dst": d, "weight": w} for s, d, w in edges + [(1, 3, 5)]]
31-
>>> bellman_ford(g, 4, 5, 0)
32-
Traceback (most recent call last):
33-
...
34-
Exception: Negative cycle found
17+
A lightweight graph class for algorithms that don't have access to the main Graph class.
3518
"""
36-
distance = [float("inf")] * vertex_count
37-
distance[src] = 0.0
38-
39-
for _ in range(vertex_count - 1):
40-
for j in range(edge_count):
41-
u, v, w = (graph[j][k] for k in ["src", "dst", "weight"])
42-
43-
if distance[u] != float("inf") and distance[u] + w < distance[v]:
44-
distance[v] = distance[u] + w
45-
46-
negative_cycle_exists = check_negative_cycle(graph, distance, edge_count)
47-
if negative_cycle_exists:
48-
raise Exception("Negative cycle found")
49-
50-
return distance
51-
52-
53-
if __name__ == "__main__":
54-
import doctest
55-
56-
doctest.testmod()
57-
58-
V = int(input("Enter number of vertices: ").strip())
59-
E = int(input("Enter number of edges: ").strip())
60-
61-
graph: list[dict[str, int]] = [{} for _ in range(E)]
62-
63-
for i in range(E):
64-
print("Edge ", i + 1)
65-
src, dest, weight = (
66-
int(x)
67-
for x in input("Enter source, destination, weight: ").strip().split(" ")
68-
)
69-
graph[i] = {"src": src, "dst": dest, "weight": weight}
70-
71-
source = int(input("\nEnter shortest path source:").strip())
72-
shortest_distance = bellman_ford(graph, V, E, source)
73-
print_distance(shortest_distance, 0)
19+
20+
def __init__(self):
21+
self.adjacency_list = defaultdict(list)
22+
self.vertices = set()
23+
24+
def add_edge(self, source: Any, destination: Any, weight: float) -> None:
25+
self.adjacency_list[source].append((destination, weight))
26+
self.vertices.add(source)
27+
self.vertices.add(destination)
28+
29+
def get_neighbors(self, vertex: Any) -> List[Tuple[Any, float]]:
30+
return self.adjacency_list.get(vertex, [])
31+
32+
def get_vertices(self) -> set:
33+
return self.vertices.copy()
34+
35+
def has_vertex(self, vertex: Any) -> bool:
36+
return vertex in self.vertices
37+
38+
def get_vertex_count(self) -> int:
39+
return len(self.vertices)
40+
41+
42+
class BellmanFord:
43+
"""
44+
Bellman-Ford algorithm implementation for single-source shortest path.
45+
46+
This algorithm can handle graphs with negative edge weights and can detect
47+
negative cycles. It's particularly useful as a preprocessing step in
48+
Johnson's algorithm.
49+
"""
50+
51+
def __init__(self, graph):
52+
"""
53+
Initialize the Bellman-Ford algorithm with a graph.
54+
55+
Args:
56+
graph: The weighted directed graph to process
57+
"""
58+
self.graph = graph
59+
self.distances = {}
60+
self.predecessors = {}
61+
62+
def find_shortest_paths(self, start_vertex: Any) -> Optional[Dict[Any, float]]:
63+
"""
64+
Find shortest paths from start_vertex to all other vertices.
65+
66+
Args:
67+
start_vertex: The source vertex to start from
68+
69+
Returns:
70+
Dictionary of vertex -> shortest distance, or None if negative cycle exists
71+
"""
72+
if not self.graph.has_vertex(start_vertex):
73+
raise ValueError(f"Start vertex {start_vertex} not found in graph")
74+
75+
# Initialize distances
76+
self.distances = {vertex: float('inf') for vertex in self.graph.get_vertices()}
77+
self.distances[start_vertex] = 0
78+
self.predecessors = {vertex: None for vertex in self.graph.get_vertices()}
79+
80+
# Relax edges V-1 times
81+
vertex_count = self.graph.get_vertex_count()
82+
83+
for iteration in range(vertex_count - 1):
84+
updated = False
85+
86+
for vertex in self.graph.get_vertices():
87+
if self.distances[vertex] != float('inf'):
88+
for neighbor, weight in self.graph.get_neighbors(vertex):
89+
new_distance = self.distances[vertex] + weight
90+
91+
if new_distance < self.distances[neighbor]:
92+
self.distances[neighbor] = new_distance
93+
self.predecessors[neighbor] = vertex
94+
updated = True
95+
96+
# Early termination if no updates in this iteration
97+
if not updated:
98+
break
99+
100+
# Check for negative cycles
101+
if self._has_negative_cycle():
102+
return None
103+
104+
return self.distances.copy()
105+
106+
def _has_negative_cycle(self) -> bool:
107+
"""
108+
Check if the graph contains a negative cycle.
109+
110+
Returns:
111+
True if negative cycle exists, False otherwise
112+
"""
113+
for vertex in self.graph.get_vertices():
114+
if self.distances[vertex] != float('inf'):
115+
for neighbor, weight in self.graph.get_neighbors(vertex):
116+
if self.distances[vertex] + weight < self.distances[neighbor]:
117+
return True
118+
return False
119+
120+
def get_path(self, start_vertex: Any, end_vertex: Any) -> Optional[List[Any]]:
121+
"""
122+
Get the shortest path from start_vertex to end_vertex.
123+
124+
Args:
125+
start_vertex: Source vertex
126+
end_vertex: Destination vertex
127+
128+
Returns:
129+
List of vertices representing the path, or None if no path exists
130+
"""
131+
if not self.distances or end_vertex not in self.distances:
132+
return None
133+
134+
if self.distances[end_vertex] == float('inf'):
135+
return None
136+
137+
path = []
138+
current = end_vertex
139+
140+
while current is not None:
141+
path.append(current)
142+
current = self.predecessors.get(current)
143+
144+
path.reverse()
145+
146+
# Verify the path starts with start_vertex
147+
if path[0] != start_vertex:
148+
return None
149+
150+
return path
151+
152+
def get_distance(self, vertex: Any) -> float:
153+
"""
154+
Get the shortest distance to a specific vertex.
155+
156+
Args:
157+
vertex: The target vertex
158+
159+
Returns:
160+
Shortest distance to the vertex
161+
"""
162+
return self.distances.get(vertex, float('inf'))
163+
164+
@staticmethod
165+
def detect_negative_cycle(graph) -> bool:
166+
"""
167+
Static method to detect if a graph contains a negative cycle.
168+
169+
Args:
170+
graph: The graph to check
171+
172+
Returns:
173+
True if negative cycle exists, False otherwise
174+
"""
175+
vertices = graph.get_vertices()
176+
if not vertices:
177+
return False
178+
179+
# Pick any vertex as start
180+
start_vertex = next(iter(vertices))
181+
bf = BellmanFord(graph)
182+
result = bf.find_shortest_paths(start_vertex)
183+
184+
return result is None

0 commit comments

Comments
 (0)