Matching problems with two distinct sides are completely solved. Pairing job applicants with positions, drivers with deliveries, students with mentors, ads with slots — all bipartite matching. Two questions arise: maximum matching (pair as many as possible) and assignment (find the pairing with the lowest total cost). Both are polynomial. Hopcroft-Karp handles unweighted matching at scale. The Hungarian algorithm — also called Kuhn-Munkres — handles cost-minimizing assignment. Rust's pathfinding library implements both as one-line calls and handles tens of thousands of items in milliseconds. When the problem has more than two sides — for example, matching student to mentor to time slot all together — it becomes the three-dimensional matching problem and is genuinely hard (NP-complete, see page 38). Two sides cheap, three sides expensive.
13
Matching two sides
Two-sided matching is a settled category. Buy the algorithm. Do not let your team build it.
-
pathfinding::kuhn_munkresis the Hungarian algorithm in one call. Feed it a square cost matrix — rows are workers, columns are jobs, cells are costs — and it returns the assignment that minimizes total cost.use pathfinding::kuhn_munkres::kuhn_munkres_min; use pathfinding::matrix::Matrix; fn assign_jobs(cost: Vec<Vec<i32>>) -> (i32, Vec<usize>) { let m = Matrix::from_rows(cost).expect("square cost matrix"); kuhn_munkres_min(&m) // (total_cost, assignment[worker] = job) } -
.expect(msg)is the "if this fails, crash with this message" method onResultandOption. ForOk(x)it returnsx; forErr(_)it panics, printingmsgon the way out. Use it when the failure case is a programming bug — something that should never happen at runtime, and if it does you want to crash loudly rather than silently keep going on corrupt data. Here,Matrix::from_rowsonly fails when the input rows have unequal lengths; if upstream code has already validated the shape, that case is impossible, and.expect("square cost matrix")documents the assumption. The alternative is the?operator — "if this isErr, return early from the containing function with that error; otherwise unwrap and continue."?requires the containing function to return a compatibleResult, so swapping.expectfor?is a function-signature change, not a one-line edit.// ── .expect — panics on Err with a message ──────────────────── fn assign_jobs_panic(cost: Vec<Vec<i32>>) -> (i32, Vec<usize>) { let m = Matrix::from_rows(cost).expect("square cost matrix"); // └──────── if rows are uneven, // the program crashes here // with the message printed // and a stack trace. kuhn_munkres_min(&m) } // ── ? operator — bubbles the Err up to the caller ───────────── // // The function's return type has to become Result for ? to work. use pathfinding::matrix::MatrixFormatError; fn assign_jobs_fallible(cost: Vec<Vec<i32>>) -> Result<(i32, Vec<usize>), MatrixFormatError> { let m = Matrix::from_rows(cost)?; // └── on Err, return early from the // whole function with that Err. // on Ok, unwrap and continue. Ok(kuhn_munkres_min(&m)) } // Compare: // .expect("…") → fail loudly, terminate the program. // Use when failure means a bug in our code. // ? → fail quietly, return Err to the caller. // Use when failure is a legitimate runtime // condition the caller should handle. // One more variant worth knowing — .unwrap() is .expect() without // a message. Convenient for prototypes; .expect() is strictly // better for production code because the panic prints useful text. - One important caveat. The
.expectand.unwrapcalls throughout this book make great examples — they fit on one line, name the failure case, and put the fallible operation on display. They are bad to copy-paste into a library. A library has no business deciding how its caller responds to failure; panicking inside library code crashes the entire process for what may be a perfectly recoverable condition. The rule for any code you publish as a crate, or any code called by code you do not own, is the same: every fallible operation returns aResult, the function signature reflects that, and the caller decides whether to retry, fall back, or propagate. Application code (main, binaries, integration tests) is allowed to panic for genuinely unrecoverable conditions — there is no caller above you, so the process crashing is the only thing left to do. Thethiserrorcrate makes the library pattern convenient: you derivestd::error::Errorand the formatting boilerplate, and the only thing you write is the variants and their messages.// ── Library code — never panics, returns Result ──────────────── use thiserror::Error; use pathfinding::matrix::MatrixFormatError; #[derive(Debug, Error)] pub enum AssignError { #[error("cost matrix rows have unequal lengths")] NotRectangular(#[from] MatrixFormatError), #[error("matrix must be square: got {rows}×{cols}")] NotSquare { rows: usize, cols: usize }, } pub fn assign_jobs(cost: Vec<Vec<i32>>) -> Result<(i32, Vec<usize>), AssignError> { let m = Matrix::from_rows(cost)?; // ? converts MatrixFormatError // via the #[from] above. if m.rows != m.columns { return Err(AssignError::NotSquare { rows: m.rows, cols: m.columns, }); } Ok(kuhn_munkres_min(&m)) } // ── Application code — .expect is fine here ─────────────────── fn main() { let cost = vec![vec![12, 8, 20], vec![ 9, 15, 11], vec![14, 10, 7]]; let (total, picks) = assign_jobs(cost).expect("known good input"); println!("total = {}, picks = {:?}", total, picks); } // Rule: // library code → return Result, never panic. thiserror // reduces a custom error type to one derive. // application code → .expect / .unwrap fine for cases the // binary author has decided are unrecoverable.