Cairn
algorithms · shortest paths · O(V · E) worst case

back to Shortest Paths

Shortest Path Faster Algorithm (SPFA)

Bellman-Ford wastes most of its own effort: every one of its V - 1 passes re-examines every edge in the graph, even edges nowhere near a distance that just changed. SPFA replaces that fixed pass structure with a FIFO queue. Start with only the source enqueued. Every time a node comes off the front of the queue, relax just its outgoing edges — and enqueue a neighbor only when its distance actually improves, and only if it isn't already waiting in the queue. Nothing gets examined that couldn't possibly still improve. The worst-case bound is unchanged from Bellman-Ford — O(V · E), since an adversarial graph can still force close to that much work — but on ordinary graphs only a small fraction of edges ever get re-checked, which is where the name comes from. On this page's own six-node demo it converges in 7 dequeues and 9 relaxation checks total, against Bellman-Ford's fixed 45 checks over 5 full passes plus a 9-check detection pass — 54 — on the identical graph.

Try it

The same six-stop shipping network as the Bellman-Ford page, starting at Depot. Press Step or Run to watch SPFA dequeue nodes from the queue strip below the graph and relax only that node's outgoing edges: the node being processed gets a bold border, an edge under examination gets highlighted, an edge that improves a distance turns orange and its target flashes, and — if that target wasn't already waiting in line — it gets appended to the back of the queue. The distance strip shows the same live shortest-known distances as the Bellman-Ford page. Press Add rebate loop to add the identical West → North route paying -12, closing the same negative cycle. This time watch the stats line: SPFA counts how many times each node gets enqueued, and the instant one reaches 6 (this graph's node count), it stops and reports a negative cycle — no waiting for a fixed extra pass. Check disable negative-cycle safeguard to see why that count matters: with it off, the queue never empties on its own. This demo caps it at 20 dequeues so the page doesn't hang; watch the distances just keep sliding further negative with no sign of stopping.

dequeues 0 · relax checks 0
Press Step or Run.

Why it works

A node's distance only ever gets smaller, never worse, and every improvement is exactly what triggers an enqueue. That's the same relaxation Bellman-Ford does — SPFA just skips the edges that plainly can't have anything to relax yet, because nothing near them has changed since they were last checked. The order nodes come off the queue doesn't matter for correctness, only for how much redundant work happens along the way; either way, the queue empties once nothing anywhere in the graph can improve any further.

The negative-cycle check reuses Bellman-Ford's own argument, just applied per node instead of via a global pass count: a genuine shortest path is simple (it never repeats a node), so it has at most V - 1 edges, and can trigger a given node's distance to improve — and so be enqueued — at most that many times as a result. A node enqueued a Vth time means the chain of relaxations that led there must have looped back through a node it had already passed through. The only way looping back could still be an improvement is if going around that loop made the distance smaller, not bigger — which is exactly what a negative-weight cycle is.

Reference implementation

Matches the demo's own logic — adjacency list per node rather than a full edge scan, so only a dequeued node's own outgoing edges ever get examined:

function spfa(numNodes, adj, source) {
  // adj[u] = [{ v, w }, ...] — outgoing edges from u
  const dist = new Array(numNodes).fill(Infinity);
  const pred = new Array(numNodes).fill(-1);
  const inQueue = new Array(numNodes).fill(false);
  const enqueueCount = new Array(numNodes).fill(0);
  dist[source] = 0;

  const queue = [source];
  inQueue[source] = true;
  enqueueCount[source] = 1;

  while (queue.length) {
    const u = queue.shift();
    inQueue[u] = false;

    for (const { v, w } of adj[u]) {
      if (dist[u] + w < dist[v]) {
        dist[v] = dist[u] + w;
        pred[v] = u;
        if (!inQueue[v]) {
          queue.push(v);
          inQueue[v] = true;
          enqueueCount[v]++;
          if (enqueueCount[v] >= numNodes) {
            return { dist, pred, negativeCycle: true, at: v };
          }
        }
      }
    }
  }

  return { dist, pred, negativeCycle: false };
}

Pitfalls

The worst case is exactly as bad as Bellman-Ford's. A queue doesn't change the asymptotic bound — it only helps on graphs where improvements don't cascade far. Adversarial graphs exist that force SPFA to re-examine close to as many edges as plain Bellman-Ford would, with extra bookkeeping overhead on top for maintaining the queue and the "already waiting" check. That's a known result from the graphs used to construct SPFA's own worst-case bound, not something this page's six-node demo is large enough to exhibit — it isn't adversarially constructed, so it shows the common-case speedup, not the worst case. This gap between "usually much faster" and "provably no better" is well documented and is the reason SPFA is treated as a practical shortcut for negative-edge graphs rather than a strict improvement on Bellman-Ford.

Unlike Dijkstra, a node can be dequeued and processed more than once — sometimes while its distance is still far from final. The demo's own default graph (no rebate loop) shows this directly: Market gets dequeued twice. The first time, its distance is still 13 — a real number, but not the eventual shortest distance of 6 — and since Market has no outgoing edges, that first dequeue does nothing useful at all. Market only gets re-enqueued once East finds a cheaper route to it later, and the second dequeue is the one that matters. How many times a node gets revisited, and in what order, depends on the graph and the order edges happen to be listed in — nothing here is predictable the way Dijkstra's "finalize once popped" guarantee is.

Skip the enqueue-count check and a negative cycle doesn't just give a wrong answer — it never terminates at all. Bellman-Ford always stops after a fixed V passes, negative cycle or not, and only then checks whether anything's still improvable. SPFA's queue has no such built-in stopping point: without the check, distances on a cycle keep shrinking forever and the queue never empties on its own. Verified directly, independently of this page's own demo: running the same graph with the rebate loop for 5,000 iterations without any safeguard left the queue still non-empty and West's distance at -5,819 and still falling, with no sign of convergence. The demo above caps the no-safeguard run at 20 dequeues for the same reason — not because the algorithm would have stopped there on its own.

Complexity

Time: O(V · E) worst case, same as Bellman-Ford — but frequently far fewer relaxation checks in practice, since only edges leaving a node whose distance just improved ever get re-examined. Space: O(V) for the distance array, predecessor array, queue-membership flags, and per-node enqueue counters.

Need correctness guarantees that don't depend on how "typical" the graph is — or a fixed, predictable amount of work every time? Bellman-Ford is the safer default; reach for SPFA when the graph has negative edges and is large enough that Bellman-Ford's fixed V - 1 passes are a real cost, not a theoretical one. For a decision guide across all eleven of this site's shortest-path entries, see Choosing a Shortest-Path Algorithm.