Bellman-Ford relaxes weighted directed edges repeatedly and can detect a negative-cost cycle reachable from a selected source.
Python Bellman-Ford: negative edges and reachable negative cycles
Operation contract
The route function accepts bounded integer node IDs and signed integer costs. An unreachable node remains None. At most V minus one relaxation rounds establish finite shortest-path costs when no reachable negative cycle exists; one more improving edge proves that a finite answer cannot be supplied for the reachable cycle. A disconnected negative cycle does not invalidate this source’s reachable routes.
Failure and ownership boundary
Dijkstra requires a different cost contract. Negative edges are accepted here, while malformed endpoints and boolean costs are rejected before relaxation. The caller receives a fresh cost list; the edge sequence is unchanged. This implementation reports a cycle error without returning a cycle witness or marking every affected destination. Python Dijkstra: nonnegative weights and stale heap entries and Python graph BFS: mark a vertex when it enters the queue use cheaper algorithms under narrower assumptions.
Working program
def signed_routes(count, edges, source):
if type(count) is not int or not 1 <= count <= 32:
raise ValueError("node budget exceeded")
if type(source) is not int or not 0 <= source < count or len(edges) > 512:
raise ValueError("invalid source or edge budget")
for start, end, weight in edges:
if any(type(value) is not int for value in (start, end, weight)):
raise ValueError("integer edges required")
if not 0 <= start < count or not 0 <= end < count or abs(weight) > 1000000:
raise ValueError("invalid edge")
costs = [None] * count
costs[source] = 0
for _ in range(count - 1):
changed = False
for start, end, weight in edges:
if costs[start] is not None:
candidate = costs[start] + weight
if costs[end] is None or candidate < costs[end]:
costs[end] = candidate
changed = True
if not changed:
break
for start, end, weight in edges:
if costs[start] is not None and (costs[end] is None or costs[start] + weight < costs[end]):
raise ValueError("reachable negative cycle")
return costs
print(signed_routes(4, [(0, 1, 5), (1, 2, -2), (0, 2, 8)], 0))
try:
signed_routes(3, [(0, 1, 1), (1, 2, -2), (2, 1, 1)], 0)
except ValueError:
print("reachable negative cycle rejected")Output
[0, 5, 3, None]
reachable negative cycle rejectedCosts and limits
Worst-case relaxation time is O(VE), with O(V) cost storage beyond the supplied edges. Early stopping helps settled inputs but does not improve the worst-case bound. Integer digit costs grow with represented values.
Common Mistakes
- Rejecting every negative edge would change this algorithm’s contract.
- An unreachable negative cycle does not affect source-specific finite costs.
Connected lessons
Python Dijkstra: nonnegative weights and stale heap entries, Python graph BFS: mark a vertex when it enters the queue, Python topological sort: dependency order and cycle rejection.
Trace the related workflow
Python Floyd-Warshall: all-pairs costs with negative-cycle rejection.
