Purpose: Find the shortest loopless paths from a source to a target in a directed graph with non-negative weights, in .
Algorithm
- Compute the shortest path with Dijkstra. Put it in the result list .
- 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 .
- For each node on the previous path (the spur node), with :
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