Integer linear programming (ILP) is linear programming with the requirement that some or all variables take integer values. This single addition pushes the problem from polynomial to NP-complete. It also makes it the most useful framework in operations research. Vehicle routing, crew scheduling, capacity planning, network design, facility location, production planning — all most naturally expressed as integer programs. Modern solvers (HiGHS, Gurobi, CPLEX) combine branch-and-bound, cutting-plane methods, primal heuristics, and presolve into engineering monuments that solve hundreds-of-thousands-of-variable problems in seconds. Rust's good_lp library exposes ILP with the same modeling interface as LP — declare some variables as integer or binary, add constraints, set objective, solve. HiGHS is the recommended open-source backend. For competitive performance on the largest problems, Gurobi or CPLEX (commercial) are state of the art.
32
When the variables must be whole numbers
Integer programming is NP-complete in theory and the most useful framework in operations research in practice. Buy the solver.
- Facility location with discrete decisions — for each candidate site, build or do not — is ILP. Same
good_lpAPI as page 19, the only change is.binary()on the variables. Modern solvers handle hundreds of thousands of binaries in seconds.use good_lp::{constraint, default_solver, ProblemVariables, SolverModel, Solution, variable}; fn pick_warehouses(open_cost: &[f64], demand: &[Vec<f64>]) -> Vec<bool> { let n = open_cost.len(); let m = demand.len(); let mut vars = ProblemVariables::new(); let open: Vec<_> = (0..n).map(|_| vars.add(variable().binary())).collect(); let model = vars .minimise(open.iter().zip(open_cost).map(|(v, &c)| v * c).sum::<good_lp::Expression>()) .using(default_solver); let mut model = model; for j in 0..m { let coverage: good_lp::Expression = (0..n).map(|i| demand[j][i] * open[i]).sum(); model = model.with(constraint!(coverage >= 1.0)); } let sol = model.solve().expect("ILP solved"); (0..n).map(|i| sol.value(open[i]) > 0.5).collect() } -
.zip()is an iterator combinator that takes two iterators and yields pairs of their elements — one from each, in lockstep.open.iter().zip(open_cost)pairs each binary variable inopenwith the corresponding cost inopen_cost, producing an iterator of(Variable, &f64)tuples. The closure|(v, &c)| v * cdestructures each pair into the variable and its cost (the&creaches past the reference, the same trick from page 15), then builds the symbolic product..sum()totals every product into a singleExpression— exactly the "minimise total cost of opened warehouses" objective. Two details worth knowing aboutzip: it stops at the shorter of the two iterators (so if the lengths differ, the tail of the longer one is dropped silently), anditertools::Itertools::zip_eqis the panic-on-mismatch variant for cases where equal length is an invariant you want enforced loudly.// .zip() — walk two iterators in lockstep, yielding pairs. let names = ["Alice", "Bob", "Carol"]; let ages = [30, 25, 40]; for (name, age) in names.iter().zip(ages.iter()) { println!("{} is {}", name, age); } // Alice is 30 // Bob is 25 // Carol is 40 // Page 32's use — pair each binary variable with its cost, // build a symbolic cost expression per pair, sum into the objective: let objective: good_lp::Expression = open.iter().zip(open_cost) .map(|(v, &c)| v * c) .sum(); // ─ ─── // │ └── &c destructures the &f64 reference, // │ same trick as for &(a, b) on page 15 // └──────── (&Variable, &f64) pair from the zipped iterators // Equivalent index-based version — works, less idiomatic: let objective: good_lp::Expression = (0..n) .map(|i| open[i] * open_cost[i]) .sum(); // // The zip version avoids the indexing arithmetic entirely. Slightly // safer (no chance of an off-by-one when the loop is rearranged) and // it reads top-to-bottom as "for each (variable, cost) pair, ..." // Important: zip stops at the SHORTER iterator. If lengths differ, // the tail of the longer one is silently dropped: let a = [1, 2, 3, 4, 5]; let b = [10, 20]; let pairs: Vec<_> = a.iter().zip(b.iter()).collect(); // = [(&1, &10), (&2, &20)] ← only two pairs; 3, 4, 5 are dropped // If "same length" is an invariant, use itertools::zip_eq — panics // on mismatch instead of silently truncating: use itertools::Itertools; let pairs: Vec<_> = a.iter().zip_eq(b.iter()).collect(); // └── panics: unequal lengths // Related combinators that operate on multiple iterators: // .zip(other) pairs in lockstep, stops at shorter // .chain(other) yields all of self, then all of other // .interleave(other) alternates: a₀ b₀ a₁ b₁ ... (itertools) // .cartesian_product(b) every (a, b) pair (itertools)