import heapq from collections.abc import Hashable Node = Hashable edge = tuple[Node, Node, float] adjacency = dict[Node, list[tuple[Node, float]]] def _collect_nodes_and_edges(graph: adjacency) -> tuple[list[Node], list[edge]]: nodes = set() edges: list[edge] = [] for u, neighbors in graph.items(): nodes.add(u) for v, w in neighbors: nodes.add(v) edges.append((u, v, w)) return list(nodes), edges def _bellman_ford(nodes: list[Node], edges: list[edge]) -> dict[Node, float]: """ Bellman-Ford relaxation to compute potentials h[v] for all vertices. Raises ValueError if a negative weight cycle exists. """ dist: dict[Node, float] = dict.fromkeys(nodes, 0.0) n = len(nodes) for _ in range(n - 1): updated = False for u, v, w in edges: if dist[u] + w < dist[v]: dist[v] = dist[u] + w updated = True if not updated: break else: for u, v, w in edges: if dist[u] + w < dist[v]: raise ValueError("Negative weight cycle detected") return dist def _dijkstra( start: Node, nodes: list[Node], graph: adjacency, potentials: dict[Node, float], ) -> dict[Node, float]: """ Dijkstra over reweighted graph, using potentials h to make weights non-negative. Returns distances from start in the reweighted space. """ inf = float("inf") dist: dict[Node, float] = dict.fromkeys(nodes, inf) dist[start] = 0.0 heap: list[tuple[float, Node]] = [(0.0, start)] while heap: d_u, u = heapq.heappop(heap) if d_u > dist[u]: continue for v, w in graph.get(u, []): w_prime = w + potentials[u] - potentials[v] if w_prime < 0: raise ValueError( "Negative edge weight after reweighting: numeric error" ) new_dist = d_u + w_prime if new_dist < dist[v]: dist[v] = new_dist heapq.heappush(heap, (new_dist, v)) return dist def johnson(graph: adjacency) -> dict[Node, dict[Node, float]]: """ Compute all-pairs shortest paths using Johnson's algorithm. Reference: https://en.wikipedia.org/wiki/Johnson%27s_algorithm Args: graph: adjacency list {u: [(v, weight), ...], ...} Returns: dict of dicts: dist[u][v] = shortest distance from u to v Raises: ValueError: if a negative weight cycle is detected Example: >>> g = { ... 0: [(1, 3), (2, 8), (4, -4)], ... 1: [(3, 1), (4, 7)], ... 2: [(1, 4)], ... 3: [(0, 2), (2, -5)], ... 4: [(3, 6)], ... } >>> round(johnson(g)[0][3], 2) 2.0 """ nodes, edges = _collect_nodes_and_edges(graph) potentials = _bellman_ford(nodes, edges) all_pairs: dict[Node, dict[Node, float]] = {} inf = float("inf") for s in nodes: dist_reweighted = _dijkstra(s, nodes, graph, potentials) dists_orig: dict[Node, float] = {} for v in nodes: d_prime = dist_reweighted[v] if d_prime < inf: dists_orig[v] = d_prime - potentials[s] + potentials[v] else: dists_orig[v] = inf all_pairs[s] = dists_orig return all_pairs