- Published on
Aspects of OR - Integer Programming (Part 1)
- Authors

- Name
- Till Heller
In the LP mini-series we learned how to optimize linear objectives subject to linear constraints, and we saw that LP solvers can handle millions of variables in seconds. But LP has a fundamental limitation: it assumes that decision variables can take any real value. In many real-world problems, this assumption does not hold. You cannot build 2.7 factories, assign 0.4 of a worker to a shift, or ship 3.14 containers to a warehouse.
Integer Programming (IP) addresses this by requiring some or all decision variables to take integer values. This seemingly small change — from to — makes the problem dramatically harder.
What makes IP hard?
Before diving into definitions, let us understand why integrality is such a big deal. The feasible region of an LP is a convex polytope — a smooth, well-behaved shape where the simplex method can slide along edges to the optimum. The feasible region of an IP is a scattered set of discrete points inside that polytope. There are no edges to follow, no gradients to exploit.
How many points are there? A binary program with variables has up to possible solutions. For , that is — more than the number of atoms in a human body. You might think: just check all of them. But even at a billion solutions per second, enumerating would take longer than the age of the universe.
LP can be solved in polynomial time. IP is NP-hard in general. Yet IP is indispensable in practice: scheduling, logistics, network design, and countless other applications require discrete decisions. And modern IP solvers routinely handle problems with thousands or millions of binary variables. How? Through clever algorithms that avoid exhaustive enumeration. This mini-series explains how:
- Part 1 (this post): Definitions, LP relaxations, and why naive approaches fail.
- Part 2: Branch and bound — systematic search with intelligent pruning.
- Part 3: Cutting planes — tightening the LP relaxation to eliminate fractional solutions.
- Part 4: Branch-and-cut and how modern solvers work.
Definitions
An Integer Linear Program (ILP or IP) has the same structure as an LP, but with integrality constraints on the variables:
A Mixed-Integer Linear Program (MIP or MILP) relaxes this by requiring only a subset of variables to be integer:
where are the integer variables and are the continuous variables.
A special case that appears constantly is Binary Programming (or 0-1 Programming), where the integer variables are restricted to . Binary variables naturally model yes/no decisions: should we open a facility? Should we include an item? Should we assign a worker to a task?
The LP relaxation
The most important tool for solving IPs is the LP relaxation: drop the integrality constraints and solve the resulting LP. If the LP relaxation of an IP is:
then the LP optimal value provides a lower bound (for minimization) on the IP optimal value. This is because every IP-feasible solution is also LP-feasible, but not vice versa — the LP feasible region is larger.
The quality of this bound — the integrality gap — varies widely across problem classes. If the LP relaxation happens to return an integer solution, we are done: the LP solution is also optimal for the IP. But in general, the LP solution will have fractional components, and we need more sophisticated methods.
Why rounding fails
The most natural idea is: solve the LP relaxation and round the fractional solution to the nearest integers. Surely that should work? Let us see what happens.
Rounding can be infeasible
Consider the constraints with . The LP relaxation might return . Rounding both up gives , which violates the constraint. Rounding both down gives , which is feasible but may be far from optimal.
Rounding can be arbitrarily bad
Consider the knapsack problem:
The LP relaxation gives , with objective . Rounding both up gives , which is infeasible (). Rounding both down gives with objective . Rounding to the nearest integer for each variable independently also gives — still infeasible. The true IP optimum is (or vice versa) with objective , but no coordinate-wise rounding rule can discover that exactly one variable should be rounded up and the other down.
Rounding fails structurally in facility location
Consider a facility location problem with 2 potential facilities and 3 customers. Opening each facility costs 100. Each customer must be served by exactly one open facility. The LP relaxation can "half-open" each facility to 50% — paying only in fixed costs — and split each customer fractionally between the two half-open facilities. Rounding both facilities up gives a fixed cost of 200, far from the LP bound. Rounding both down means no facility is open and no customer can be served — the solution is infeasible. The LP relaxation exploits the fractional structure in a way that no integer rounding can replicate.
These examples show that we need principled algorithms — not ad hoc rounding — to solve IPs.
Example 1: Uncapacitated facility location
Let us start with the richer of our two examples. A company must decide which of potential facility locations to open and how to assign customers to open facilities. Opening facility incurs a fixed cost . Serving customer from facility costs . Each customer must be assigned to exactly one facility.
subject to
This is a mixed-integer program: the facility decisions are binary, but the assignment variables are continuous. Once the binary decisions are fixed, the assignment becomes a simple LP that naturally produces integer solutions (a property of the constraint structure).
Solving with Gurobi
import gurobipy as gp
from gurobipy import GRB
facilities = [1, 2, 3]
customers = ['A', 'B', 'C', 'D']
fixed_cost = {1: 100, 2: 150, 3: 120}
serve_cost = {
(1, 'A'): 10, (1, 'B'): 30, (1, 'C'): 25, (1, 'D'): 40,
(2, 'A'): 25, (2, 'B'): 10, (2, 'C'): 20, (2, 'D'): 15,
(3, 'A'): 30, (3, 'B'): 20, (3, 'C'): 10, (3, 'D'): 20,
}
model = gp.Model('facility_location')
y = model.addVars(facilities, vtype=GRB.BINARY, name='open')
x = model.addVars(serve_cost.keys(), lb=0, name='assign')
model.setObjective(
gp.quicksum(fixed_cost[i] * y[i] for i in facilities) +
gp.quicksum(serve_cost[i, j] * x[i, j] for i, j in serve_cost),
GRB.MINIMIZE
)
for j in customers:
model.addConstr(gp.quicksum(x[i, j] for i in facilities) == 1, f'demand_{j}')
for i, j in serve_cost:
model.addConstr(x[i, j] <= y[i], f'link_{i}_{j}')
model.optimize()
for i in facilities:
if y[i].X > 0.5:
served = [j for j in customers if x[i, j].X > 0.5]
print(f'Facility {i} open, serves: {served}')
print(f'Total cost: {model.ObjVal:.0f}')
The LP relaxation of this problem "half-opens" facilities and fractionally splits customers, yielding a lower bound. The MIP solver uses branch and bound and cutting planes internally to find the optimal integer solution.
Example 2: The 0-1 knapsack problem
The knapsack problem is one of the most fundamental IPs. A thief breaks into a warehouse and can carry a knapsack with weight capacity . There are items, each with a weight and a value . Which items should be selected to maximize total value?
A concrete instance
| Item | Weight | Value | Value/Weight |
|---|---|---|---|
| 1 | 6 | 16 | 2.67 |
| 2 | 8 | 22 | 2.75 |
| 3 | 5 | 12 | 2.40 |
| 4 | 4 | 8 | 2.00 |
Capacity .
LP relaxation: Greedily fill by value-to-weight ratio. Take item 2 (ratio 2.75, weight 8), then of item 1 (ratio 2.67, weight 5). Total: weight , LP value .
IP optimum: Take items 2 and 3 (weight , value ). The LP bound of 35.33 is close but not achievable by any integer solution.
We will use this exact instance in Part 2 to walk through branch and bound step by step.
Solving with Gurobi
import gurobipy as gp
from gurobipy import GRB
items = [1, 2, 3, 4]
weight = {1: 6, 2: 8, 3: 5, 4: 4}
value = {1: 16, 2: 22, 3: 12, 4: 8}
capacity = 13
model = gp.Model('knapsack')
x = model.addVars(items, vtype=GRB.BINARY, name='x')
model.setObjective(gp.quicksum(value[j] * x[j] for j in items), GRB.MAXIMIZE)
model.addConstr(gp.quicksum(weight[j] * x[j] for j in items) <= capacity, 'capacity')
model.optimize()
for j in items:
if x[j].X > 0.5:
print(f'Item {j}: weight={weight[j]}, value={value[j]}')
print(f'Total value: {model.ObjVal:.0f}')
Output: items 2 and 3 are selected, total value 34.
The role of formulation strength
Not all IP formulations for the same problem are equally good. A stronger formulation is one whose LP relaxation provides a tighter bound. Stronger formulations lead to tighter bounds, more pruning, and faster solve times. Finding strong formulations is a key skill in integer programming.
Let us see this in action on the facility location problem. The formulation above uses individual linking constraints for each facility-customer pair. An alternative uses a single aggregate constraint per facility:
Both formulations are correct — they have the same set of integer-feasible solutions. But the aggregate formulation is weaker: it allows the LP relaxation to open a facility to just and still assign one full customer to it.
Consider a small instance with 2 facilities and 3 customers, fixed costs , and service costs:
| Cust. 1 | Cust. 2 | Cust. 3 | |
|---|---|---|---|
| Facility 1 | 10 | 30 | 25 |
| Facility 2 | 25 | 15 | 10 |
The strong formulation () gives an LP bound of 130 — which turns out to be the IP optimum itself. The LP relaxation directly finds the integer solution: open both facilities, cost (sic). No branch and bound needed.
The weak formulation () gives an LP bound of only 121.67. The LP relaxation "half-opens" facility 1 to and facility 2 to , saving on fixed costs. The solver must now branch and explore multiple nodes to close the gap.
Same problem, same IP optimum — but the stronger formulation solves instantly. We will see in Part 3 how cutting planes can strengthen a formulation dynamically during the solve.