N or P
Contents Glossary
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.

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.

  1. 01
    Facility location with discrete decisions — for each candidate site, build or do not — is ILP. Same good_lp API 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()
    }
  2. 02
    .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 in open with the corresponding cost in open_cost, producing an iterator of (Variable, &f64) tuples. The closure |(v, &c)| v * c destructures each pair into the variable and its cost (the &c reaches past the reference, the same trick from page 15), then builds the symbolic product. .sum() totals every product into a single Expression — exactly the "minimise total cost of opened warehouses" objective. Two details worth knowing about zip: it stops at the shorter of the two iterators (so if the lengths differ, the tail of the longer one is dropped silently), and itertools::Itertools::zip_eq is 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)
Karp, R. (1972). Gomory, R. (1958) introduced cutting planes. Land, A., Doig, A. (1960) introduced branch-and-bound. Source →
In plain terms

Integer linear programming is linear programming with one extra rule: some variables must be whole numbers (or, in the binary case, 0 or 1). That single rule transforms the problem from polynomial-time (LP, page 19) to NP-complete. It also transforms it into the most useful framework in operations research.

The reason ILP shows up so often is that real business decisions are discrete. Either you build the warehouse or you do not. Either this employee works the shift or they do not. Either the truck makes the stop or it does not. Continuous variables cannot model these decisions; integer variables can.

The reason ILP is NP-complete is that the feasible set is no longer a smooth shape but a discrete lattice inside one. The fast algorithms that work for LP — walking vertices of the polyhedron, interior-point methods — do not apply. In the worst case, you have to search an exponential tree of "this variable rounds up or down."

In practice, modern ILP solvers are engineering monuments. They combine branch-and-bound (the exponential tree), cutting planes (added valid inequalities that tighten the relaxation without removing integer points), primal heuristics (find good feasible solutions quickly to bound the search), and presolve (eliminate redundant variables and constraints). The result is that practical ILPs with hundreds of thousands of binary variables routinely solve in seconds.

In Rust, good_lp is the modeling library. Declare variables (.binary(), .integer(), or continuous), add constraints with operator overloading, set the objective, call solve, pick a backend. HiGHS is the recommended open-source backend. For the largest problems, Gurobi and CPLEX (commercial) remain state of the art and are worth the license cost when the optimization is core to the business.

The teaching for planning: when your problem has discrete decisions and linear cost structure, model it as ILP and try the solver. Modern MIP solvers handle a startling range of operational problems on commodity hardware. The engineering effort is in modeling the problem correctly (right variables, right constraints, right objective), not in writing search code.

ILP is NP-complete. ILP solvers are great. Both facts are true.

← 31 Bin packing — fit the items into the fewest containers
32/39
33 Approximation — provably close to optimal →