When you need to connect a set of points — fiber backbone, electrical grid, road network, communication mesh — with the minimum total length of cable or path, the problem is a minimum spanning tree. Two algorithms find the exact best answer: sort all possible connections by cost and add them in order, skipping any that would form a redundant loop (Kruskal 1956); or grow a tree from any starting point, always adding the cheapest connection to a new point (Prim 1957). Both are fast — work grows roughly with the number of possible connections. Rust's petgraph has both. When the problem changes to "connect this subset, using others as relays if it saves money," it becomes the Steiner tree problem and is genuinely hard (NP-complete, see page 38 for the contrast). The line between cheap and expensive is whether every point must be connected or only a subset.
11
Cheapest network that connects everything
Connect every point with the cheapest wires that form no loop. This is one of the few business problems where greedy is provably optimal.
-
min_spanning_treereturns the edges of the optimal tree as an iterator you can fold straight into a new graph. For a fiber-rollout plan, the input is locations with cable costs and the output is the cheapest provably-complete network.use petgraph::algo::min_spanning_tree; use petgraph::data::FromElements; use petgraph::graph::UnGraph; fn cheapest_fiber(sites: &UnGraph<&str, u32>) -> UnGraph<&str, u32> { UnGraph::from_elements(min_spanning_tree(sites)) } - A turbofish is the syntax
::<T>— Rust's way of specifying generic type parameters at a callsite. Most of the time the compiler infers generic parameters from context, so you do not write the turbofish; it is the escape hatch for when inference is not enough. The name is folklore:::<>looks like a tiny fish swimming to the right — two dots for eyes, angle brackets for fins. The callUnGraph::from_elements(min_spanning_tree(sites))above works without a turbofish because the function signature-> UnGraph<&str, u32>pins down the type parameters for inference. Strip the signature away and the turbofish has to surface.// Anatomy: // // parse :: < i32 > () // ───── ━━ ━━━━━━━ ── // name type call // ┗━━━━━┳━━━━┛ // └─ turbofish: ::<T> // // Two dots (eyes), two angle brackets (fins). ><> ::<T> // Two places it commonly appears: // 1. parse() — the return type is the only thing that decides which // parser to call, and the compiler can't always infer it. let n = "42".parse::<i32>().unwrap(); // turbofish on parse let n: i32 = "42".parse().unwrap(); // or annotate the binding // 2. collect() — the iterator gives no hint about which container // you want to materialise into. let v = (1..=5).collect::<Vec<i32>>(); // turbofish on collect let v: Vec<i32> = (1..=5).collect(); // or annotate the binding // Page 11's call works without it because the function's return type // pins down the generics: fn cheapest_fiber(sites: &UnGraph<&str, u32>) -> UnGraph<&str, u32> { UnGraph::from_elements(min_spanning_tree(sites)) // inferred } // Same thing written with the turbofish — equivalent, more verbose: fn cheapest_fiber_explicit(sites: &UnGraph<&str, u32>) -> UnGraph<&str, u32> { UnGraph::<&str, u32>::from_elements(min_spanning_tree(sites)) // ▲▲▲▲▲▲▲▲▲▲▲▲ // the turbofish }