Yen's Algorithm (K Shortest Loopless Paths) — Practice Tasks¶
All tasks must be solved in Go, Java, and Python. Each task ships with a problem statement, constraints, hints, and starter code in all three languages. Yen's core loop is: seed
Awith one Dijkstra, then for each spur node on the latest accepted path, remove repeating edges + root interior nodes, compute a spur path, pushroot + spurinto the candidate heapB, and promote the cheapest. Weights are non-negative throughout (Yen relies on Dijkstra).
Beginner Tasks (5)¶
Task 1 — Dijkstra with removed edges and nodes¶
Problem. Implement the inner subroutine Yen depends on: a Dijkstra that returns the shortest s→t path (vertex list + cost) while ignoring a set of removed edges and removed nodes. Return a "not found" signal if t is unreachable.
Constraints. Non-negative weights, 1 ≤ V ≤ 10^4. Must be O(E + V log V) with a binary heap.
Hint. Skip any edge (u,v) with v in removedNodes or (u,v) in removedEdges. Reconstruct the path with a prev map.
Go¶
package main
// dijkstra returns (path, cost, found).
func dijkstra(g map[string]map[string]int, s, t string,
remE map[[2]string]bool, remN map[string]bool) ([]string, int, bool) {
// implement here
return nil, 0, false
}
func main() {
// test here
}
Java¶
import java.util.*;
public class Task1 {
// return int[]{cost} and fill outPath; null if not found
static int[] dijkstra(Map<String, Map<String, Integer>> g, String s, String t,
Set<String> remE, Set<String> remN, List<String> outPath) {
// implement here
return null;
}
public static void main(String[] args) {
// test here
}
}
Python¶
import heapq
def dijkstra(g, s, t, rem_e=frozenset(), rem_n=frozenset()):
# implement here; return (path, cost) or None
pass
if __name__ == "__main__":
pass
- Expected: on
C→E 2, E→F 2, F→H 1,dijkstra(g,"C","H")returns(["C","E","F","H"], 5). - Evaluation: correctness, handles removals, returns "not found" cleanly.
Task 2 — Path cost helper¶
Problem. Given a graph and a vertex list, compute the total cost of the path (sum of consecutive edge weights). Assume every consecutive pair is a real edge.
Constraints. O(path length).
Hint. Loop over indices 0 .. len-2 and sum g[p[i]][p[i+1]].
Go¶
Java¶
static int pathCost(Map<String, Map<String, Integer>> g, List<String> p) {
// implement here
return 0;
}
Python¶
- Expected: cost of
["C","E","F","H"]on the Task 1 graph is5. - Evaluation: correct sum, handles a single-vertex path (cost 0).
Task 3 — Seed the accepted list¶
Problem. Write the first step of Yen: run Dijkstra once from s to t and return the accepted list A containing just that path (or empty if unreachable).
Constraints. Reuse Task 1's Dijkstra.
Hint. A = [dijkstra(g, s, t)] if found, else [].
Go¶
Java¶
static List<List<String>> seed(Map<String, Map<String, Integer>> g, String s, String t) {
// implement here
return new ArrayList<>();
}
Python¶
- Expected:
seedreturns one path on a connected graph, empty list whenscan't reacht. - Evaluation: correct base case for the algorithm.
Task 4 — Compute the removal sets for one spur node¶
Problem. Given the accepted list A, the latest path, and a spur index i, compute (a) the set of edges to remove (the spur-leaving edge of every path in A sharing the root last[:i+1]) and (b) the set of root interior nodes to remove (last[:i]).
Constraints. O(|A| · path length).
Hint. A path p shares the root iff p[:i+1] == last[:i+1]; then remove edge (p[i], p[i+1]).
Go¶
func removals(A [][]string, last []string, i int) (map[[2]string]bool, map[string]bool) {
// implement here
return nil, nil
}
Java¶
static Object[] removals(List<List<String>> A, List<String> last, int i) {
// return {Set<String> remE (as "u>v"), Set<String> remN}
return null;
}
Python¶
- Expected: for
last=["C","E","F","H"],A=[last],i=1⇒ remove edge("E","F")and node"C". - Evaluation: removes the right edges/nodes; keeps the spur node itself.
Task 5 — Reproduce the worked example¶
Problem. Using Tasks 1–4 (or a full Yen implementation), find the 3 shortest loopless paths from C to H on the graph below and print each path with its cost.
Constraints. Output must be [5, 7, 8] with paths C,E,F,H / C,E,G,H / C,D,F,H.
Hint. Wire the pieces: seed, loop over spur nodes, build candidates into a min-heap, pop the cheapest.
Go¶
func yen(g map[string]map[string]int, s, t string, k int) [][2]interface{} {
// return list of (nodes, cost)
return nil
}
Java¶
static void yen(Map<String, Map<String, Integer>> g, String s, String t, int k) {
// print each path and cost
}
Python¶
- Expected: the three paths and costs above, in order.
- Evaluation: correct ordering, dedup, dead-end handling.
Intermediate Tasks (5)¶
Task 6 — Full Yen with deduplication¶
Problem. Implement complete Yen's algorithm that returns up to K shortest loopless paths, deduplicating candidates so no path appears twice in A or B.
Constraints. O(K · V · (E + V log V)). Use a seen set keyed on the path.
Hint. Dedup on push: only heappush a candidate if its key isn't in seen.
Go¶
func yenDedup(g map[string]map[string]int, s, t string, k int) [][]string {
// implement full algorithm with a seen-set
return nil
}
Java¶
static List<List<String>> yenDedup(Map<String, Map<String, Integer>> g,
String s, String t, int k) {
// implement
return new ArrayList<>();
}
Python¶
- Constraints: verify no duplicate paths; return fewer than K when fewer exist.
- Evaluation: correctness on graphs with multiple equal-cost paths.
Task 7 — Lawler's deviation-index optimization¶
Problem. Extend Task 6 to store a deviation index per accepted path and start spur generation from that index, skipping redundant spur nodes.
Constraints. Output must be identical to plain Yen; only fewer Dijkstra calls.
Hint. Store (cost, nodes, devIdx). For the latest path, iterate spur indices devIdx .. len-2.
Go¶
type Cand struct {
Cost int
Nodes []string
Dev int
}
func yenLawler(g map[string]map[string]int, s, t string, k int) []Cand {
// implement
return nil
}
Java¶
record Cand(int cost, List<String> nodes, int dev) {}
static List<Cand> yenLawler(Map<String, Map<String, Integer>> g, String s, String t, int k) {
return new ArrayList<>();
}
Python¶
- Constraints: compare spur-Dijkstra counts with/without Lawler on a graph with long shared prefixes.
- Evaluation: same output, measurably fewer spur calls.
Task 8 — K shortest with a cost ceiling¶
Problem. Return all loopless paths within a factor (1 + ε) of the shortest path's cost (instead of a fixed K). Stop early when the heap minimum exceeds the ceiling.
Constraints. Use the nondecreasing-order property to terminate.
Hint. Ceiling = cost(A[0]) * (1 + eps). Stop when peek(B).cost > ceiling.
Go¶
func yenCeiling(g map[string]map[string]int, s, t string, eps float64) [][]string {
// implement
return nil
}
Java¶
static List<List<String>> yenCeiling(Map<String, Map<String, Integer>> g,
String s, String t, double eps) {
return new ArrayList<>();
}
Python¶
- Constraints: must not compute candidates beyond the ceiling.
- Evaluation: correct early termination, returns exactly the within-ceiling paths.
Task 9 — Return paths, not just costs¶
Problem. Modify your Yen to return both the vertex sequence and the cost of each of the K paths, formatted as C -> E -> F -> H (cost 5).
Constraints. Reconstruct paths from the candidate structure, not from a separate run.
Hint. Store the full node list with each accepted entry; format on output.
Go¶
func yenWithPaths(g map[string]map[string]int, s, t string, k int) []string {
// return formatted strings
return nil
}
Java¶
static List<String> yenWithPaths(Map<String, Map<String, Integer>> g,
String s, String t, int k) {
return new ArrayList<>();
}
Python¶
- Expected: formatted route strings in nondecreasing cost order.
- Evaluation: correct reconstruction and formatting.
Task 10 — Handle the zero-weight cycle corner case¶
Problem. On a graph containing a zero-weight cycle through a potential spur node, ensure your Yen never emits a non-simple path. Add the spur-node re-entry guard (settle the spur node at distance 0, forbid returning to it).
Constraints. Test on a graph with edges A→B 0, B→A 0 on a root path.
Hint. During a spur search from v, also remove incoming edges to v (or mark v settled and never relax into it).
Go¶
func dijkstraGuarded(g map[string]map[string]int, spur, t string,
remE map[[2]string]bool, remN map[string]bool) ([]string, int, bool) {
// forbid re-entering spur node
return nil, 0, false
}
Java¶
static int[] dijkstraGuarded(Map<String, Map<String, Integer>> g, String spur, String t,
Set<String> remE, Set<String> remN, List<String> outPath) {
// forbid re-entering spur node
return null;
}
Python¶
def dijkstra_guarded(g, spur, t, rem_e=frozenset(), rem_n=frozenset()):
# forbid re-entering the spur node even at zero cost
pass
- Constraints: every returned path must be simple.
- Evaluation: correctly rejects the zero-cycle loop.
Advanced Tasks (5)¶
Task 11 — Diverse K paths (greedy dissimilarity)¶
Problem. Over-fetch 3K exact paths with Yen, then greedily select K whose pairwise edge Jaccard overlap stays below a threshold (e.g. 0.6), preserving cheapest-first order.
Constraints. Output is the K-cheapest-subject-to-diversity, not the exact K-cheapest.
Hint. Compute edge sets set(zip(p, p[1:])); skip a candidate too similar to any already-chosen.
Go¶
func diverseK(g map[string]map[string]int, s, t string, k int, maxOverlap float64) [][]string {
// implement over-fetch + greedy select
return nil
}
Java¶
static List<List<String>> diverseK(Map<String, Map<String, Integer>> g,
String s, String t, int k, double maxOverlap) {
return new ArrayList<>();
}
Python¶
def diverse_k(g, s, t, k, max_overlap=0.6):
# over-fetch 3k, greedily select k by Jaccard dissimilarity
return []
- Evaluation: returned paths are genuinely different; still cheapest-first within the diverse set.
Task 12 — Yen on an undirected graph¶
Problem. Adapt Yen to undirected weighted graphs (each edge usable both directions) and verify all results are loopless.
Constraints. Represent each undirected edge {u,v,w} as two directed edges.
Hint. Build the directed adjacency from undirected input; the rest of Yen is unchanged.
Go¶
func buildUndirected(edges [][3]interface{}) map[string]map[string]int {
// add both directions
return nil
}
Java¶
static Map<String, Map<String, Integer>> buildUndirected(List<Object[]> edges) {
return new HashMap<>();
}
Python¶
- Evaluation: correct K loopless paths; no path revisits a vertex.
Task 13 — Parallel spur computation¶
Problem. Parallelize the spur Dijkstras within one iteration across worker threads, merging candidates into a single guarded heap, while keeping the cross-iteration order correct.
Constraints. The graph is read-only; guard only B and the seen set.
Hint. Fan out spur jobs (map), collect candidates, merge under a lock (reduce), then pop the next accepted path.
Go¶
import "sync"
func yenParallel(g map[string]map[string]int, s, t string, k int) [][]string {
var mu sync.Mutex
var wg sync.WaitGroup
_ = mu
_ = wg
// fan out spurs per iteration
return nil
}
Java¶
import java.util.concurrent.*;
static List<List<String>> yenParallel(Map<String, Map<String, Integer>> g,
String s, String t, int k) throws Exception {
// use an ExecutorService for spur jobs
return new ArrayList<>();
}
Python¶
from concurrent.futures import ThreadPoolExecutor
def yen_parallel(g, s, t, k, workers=8):
# fan out spur jobs per iteration, merge into a guarded heap
return []
- Evaluation: identical output to serial Yen; correct synchronization, no data races.
Task 14 — Compare Yen vs Eppstein on a DAG¶
Problem. On a directed acyclic graph, implement both Yen and a simple Eppstein-style k-best (loops impossible on a DAG, so both give loopless paths) and compare their running time for increasing K.
Constraints. Demonstrate Eppstein's near-linear-in-K advantage.
Hint. On a DAG, you can compute dist(v, t) for all v by reverse topological DP, then enumerate sidetracks. Yen is your baseline.
Go¶
func benchYenVsEppstein(dag map[string]map[string]int, s, t string, ks []int) {
// time both for each K and print
}
Java¶
static void benchYenVsEppstein(Map<String, Map<String, Integer>> dag,
String s, String t, int[] ks) {
// time both for each K
}
Python¶
- Evaluation: correct results from both; clear demonstration of the complexity gap as K grows.
Task 15 — Constrained KSP (hop limit)¶
Problem. Find the K shortest loopless paths whose length is at most L hops. Filter candidates exceeding the hop limit before accepting.
Constraints. A path with > L edges is rejected (not merely deprioritized).
Hint. Either bound the spur Dijkstra's depth, or reject over-long totals before pushing into B. Discuss why a cost budget on a second metric would instead be NP-hard.
Go¶
func yenHopLimited(g map[string]map[string]int, s, t string, k, maxHops int) [][]string {
// reject paths exceeding maxHops
return nil
}
Java¶
static List<List<String>> yenHopLimited(Map<String, Map<String, Integer>> g,
String s, String t, int k, int maxHops) {
return new ArrayList<>();
}
Python¶
def yen_hop_limited(g, s, t, k, max_hops):
# reject candidates with more than max_hops edges
return []
- Evaluation: all returned paths respect the hop limit; correct K-best among them.
Benchmark Task¶
Compare Yen's running time across all 3 languages as
Kand graph size grow.
Go¶
package main
import (
"fmt"
"math/rand"
"time"
)
func makeGraph(n, deg int) map[string]map[string]int {
g := map[string]map[string]int{}
for u := 0; u < n; u++ {
key := fmt.Sprintf("%d", u)
g[key] = map[string]int{}
for d := 0; d < deg; d++ {
v := rand.Intn(n)
if v != u {
g[key][fmt.Sprintf("%d", v)] = rand.Intn(9) + 1
}
}
}
return g
}
func main() {
for _, n := range []int{100, 500, 1000} {
g := makeGraph(n, 4)
start := time.Now()
// yenDedup(g, "0", fmt.Sprintf("%d", n-1), 10)
_ = g
fmt.Printf("n=%5d K=10: %v\n", n, time.Since(start))
}
}
Java¶
import java.util.*;
public class Benchmark {
public static void main(String[] args) {
Random rnd = new Random();
for (int n : new int[]{100, 500, 1000}) {
Map<String, Map<String, Integer>> g = new HashMap<>();
for (int u = 0; u < n; u++) {
Map<String, Integer> adj = new HashMap<>();
for (int d = 0; d < 4; d++) {
int v = rnd.nextInt(n);
if (v != u) adj.put(String.valueOf(v), rnd.nextInt(9) + 1);
}
g.put(String.valueOf(u), adj);
}
long start = System.nanoTime();
// yenDedup(g, "0", String.valueOf(n - 1), 10);
System.out.printf("n=%5d K=10: %.3f ms%n", n, (System.nanoTime() - start) / 1e6);
}
}
}
Python¶
import random
import time
def make_graph(n, deg):
g = {}
for u in range(n):
g[str(u)] = {}
for _ in range(deg):
v = random.randrange(n)
if v != u:
g[str(u)][str(v)] = random.randint(1, 9)
return g
for n in (100, 500, 1000):
g = make_graph(n, 4)
start = time.perf_counter()
# yen_dedup(g, "0", str(n - 1), 10)
print(f"n={n:>5} K=10: {(time.perf_counter() - start) * 1000:.3f} ms")
- Goal: observe the
K·V·(E + V log V)scaling; note where pure Python lags Go/Java and where Lawler's optimization helps.