Source: "Breaking the Sorting Barrier for Directed Single-Source Shortest Paths" (Duan, Mao, Mao, Shu, Yin, 2025; arXiv 2504.17033v2; ACM 10.1145/3717823.3718179).
A deterministic O(m·log^(2/3) n) algorithm for single-source shortest paths (SSSP) on directed graphs with real, non-negative edge weights, in the comparison-addition model (only comparisons and additions on weights, each O(1)). This is the first algorithm to beat Dijkstra's classic O(m + n·log n) bound on sparse graphs — i.e. it proves Dijkstra is not optimal for SSSP when you only need distances, not a full sorted order.
n= number of vertices,m= number of edges.- The improvement matters on sparse graphs where
m = O(n).
Dijkstra repeatedly extracts the closest vertex from a priority queue and relaxes its edges. As a byproduct it produces vertices in sorted distance order. Producing a total sorted order of n items costs Ω(n log n) — the "sorting barrier." Haeupler et al. (2024) showed Dijkstra is optimal if you must output that order. The trick here: we don't need the order, only the distances, so we can avoid fully sorting the frontier.
- At a moment in Dijkstra, the priority queue holds a frontier
S. Every incomplete vertexuwhose true distanced(u) < Bhas a shortest path that must pass through some complete vertex inS. We say such auis "dependent on"S. - The cost blows up because
Scan hold Θ(n) vertices and we keep re-sorting them. - Key move — shrink the frontier. Suppose we only care about distances
< B. LetU'be the verticesuwithd(u) < Breachable throughS. We can always reduce the useful frontier to size|U'| / kfor a parameterk = log^Ω(1)(n):- If
|U'| > k·|S|, the frontier is already small relative toU'(≤ |U'|/k). - Otherwise run Bellman-Ford for k steps from
S. Any vertex whose shortest path uses< kintermediate frontier-region vertices becomes complete. The rest must hang off a shortest-path tree with≥ kvertices — so only the roots ("pivots") of those big trees matter, and there are at most|U'|/kof them.
- If
- Instead of a dynamic Dijkstra frontier (intractable to keep small), use divide-and-
conquer with
O((log n)/t)levels. Each level has a frontier and a boundB. Frontier reduction means the expensive Θ(t) work per vertex applies only to the ~1/log^Ω(1)(n)pivots — dropping cost per vertex fromlog ntolog n / log^Ω(1) n.
k = ⌊ log^(1/3)(n) ⌋ // "steps" of Bellman-Ford in FindPivots; base-case batch size
t = ⌊ log^(2/3)(n) ⌋ // controls the branching / recursion depth
- Recursion has levels
l ∈ [0, ⌈(log n)/t⌉]. Depth isO((log n)/t) = O(log^(1/3) n). - At level
l, the frontier obeys|S| ≤ 2^(l·t), and the block-list parameter isM = 2^((l-1)·t). - Top-level call:
BMSSP(l = ⌈(log n)/t⌉, B = ∞, S = {s}). Because|U| ≤ |V|this must be a "successful execution," and all distances get computed.
Practical note (from the explainer): these asymptotics only win for astronomically large n. A straightforward implementation runs ~5–7× slower than Dijkstra in practice. The value of this repo is a correct, readable implementation, not raw speed.
- Constant-degree graph. The paper first transforms
Gso every vertex has in-degree and out-degree ≤ 2 (split each vertex into a small zero-weight cycle). This keepsm = O(n)and makes the "constant out-degree" arguments hold. An implementation may skip this transform and just handle general degrees; it affects constants, not correctness. - All vertices reachable from
sis assumed (som ≥ n − 1). - Distinct path lengths (Assumption 2.1). The paper assumes every path has a unique
length, giving a strict total order on paths. This keeps the predecessor pointers
Pred[]a proper tree and breaks ties deterministically. In code, ties can be broken by an explicit rule (e.g. by (length, #vertices, vertex sequence)) — you rarely need the full machinery, but be aware equal distances need a consistent tie-break. - Labels.
d̂[v]starts at 0 for the source, ∞ elsewhere, and only ever decreases via edge relaxationd̂[v] ← min(d̂[v], d̂[u] + w(u,v)).Pred[v]records the predecessor.
d̂[·]⇢ theshortestPathsMap insrc/bmssp.mjs.- The Dijkstra oracle ⇢
src/dijkstra.mjs(already implemented; used in tests). k,t⇢ to be derived fromn = nodeIDs.sizewhen the algorithm is built.