Purpose: Find the shortest loopless paths from a source to a target in a directed graph with non-negative weights, in .

Algorithm

  1. Compute the shortest path with Dijkstra. Put it in the result list .
  2. For :
    • For each node on the previous path (the spur node), with :
      • Remove every edge for each already-found path sharing the same root prefix. This forbids re-deriving a known path.
      • Remove every node of the root path except the spur node itself. This forbids loops.
      • Run Dijkstra from the spur node to the target. If a path is found, the candidate is ; add it to a candidate set (a min-heap keyed by total cost).
      • Restore the removed edges and nodes.
    • If is empty, stop — fewer than paths exist. Otherwise move the cheapest candidate from to .

Code sketch

vector<Path> yen(int s, int t, int K) {
    vector<Path> A;
    A.push_back(dijkstraPath(s, t));
    if (A[0].empty()) return {};
    set<pair<long long, Path>> B;                 // candidates, ordered by cost
 
    for (int k = 1; k < K; k++) {
        const Path& prev = A[k - 1];
        for (int i = 0; i + 1 < (int)prev.size(); i++) {
            int spur = prev[i];
            Path root(prev.begin(), prev.begin() + i + 1);
 
            vector<Edge> removedE; vector<int> removedV;
            for (const Path& p : A)
                if (p.size() > i && equalPrefix(p, root, i + 1))
                    removedE.push_back(removeEdge(p[i], p[i + 1]));
            for (int j = 0; j < i; j++) removedV.push_back(removeNode(root[j]));
 
            Path spurPath = dijkstraPath(spur, t);
            if (!spurPath.empty()) {
                Path total = root; total.pop_back();
                total.insert(total.end(), spurPath.begin(), spurPath.end());
                B.insert({cost(total), total});
            }
            restore(removedE, removedV);
        }
        if (B.empty()) break;
        A.push_back(B.begin()->second);
        B.erase(B.begin());
    }
    return A;
}

Paradigm

Deviation / branch enumeration. Every -th shortest path must deviate from one of the first paths at some node; Yen’s enumerates exactly those deviations and takes the best.

Complexity

  • Time: — up to spur nodes per round, each costing a Dijkstra
  • Space:

Why It Works

Claim: the -th shortest loopless path shares a (possibly empty) prefix with one of and then deviates.

Any path that shares no prefix with still shares the source, i.e. deviates at index 0 — which is a spur node the algorithm tries. Since the algorithm generates, for every already-found path and every deviation point, the cheapest continuation avoiding known paths and loops, the true -th path is always present in when it is needed. Taking the minimum of therefore yields it. ∎

Variants / Use Cases

  • Eppstein’s algorithm but allows loops; strictly faster when loopy paths are acceptable
  • Suurballe’s algorithm vertex-disjoint paths, a different requirement
  • Network routing with backup paths, transit itineraries, k-best alignments in bioinformatics
  • k-shortest walks in a DAG — much simpler: a DP keeping the best values per node
  • Lawler’s modification — avoids redundant spur computations by remembering the deviation index; a constant-factor improvement worth having