Easier to read with cream paper, a tint, more contrast or larger text? Choose your colours and text, once, for every page.

Free lesson

Integer programming and the knapsack problem

After this lesson: I can say why whole-number problems are harder than ordinary linear programs.

60 min lesson3 commandsJupyterLab data science lab: pandas, Matplotlib and scikit-learn in the browser

Why whole numbers make a problem harder

An integer program is a linear program in which some or all of the variables must be whole numbers. You cannot hire half a driver or bake 1.5 cakes to sell, so many real problems are of this kind.

They look like small changes and they are much harder to solve. You will see why rounding the easy answer fails.

By the end of this lesson

Note

I can say why whole-number problems are harder than ordinary linear programs.

Key words

WordWhat it means
Integer programa model whose variables must be whole numbers
Relaxationthe same model with the whole-number rule dropped
Roundingmoving an answer to the nearest whole numbers
Knapsack problemchoosing items to fit a weight limit with the most value
Dynamic programmingsolving small cases first and reusing their answers

The cells students fill in are left blank in this preview.

The best plan may need half a cake

The bakery plan from lesson 3 is 5 loaf batches and 1.5 cake batches. If a batch cannot be split, that plan is not allowed.

The model without the whole-number rule is called the relaxation. Its best answer is always at least as good as the best whole-number answer.

Rounding can break a limit or lose profit

The easy idea is to round 1.5 to a whole number. Rounding up gives 5 and 2, which needs 9 oven hours and breaks the limit. Rounding down gives 5 and 1, which is feasible but earns only 130 pounds.

The best whole plan is 4 and 2, which earns 140. It is not close to the rounded answer.

Compare three plans

Command
docker exec jupyter python3 -c "print(20 * 5 + 30 * 1, 20 * 4 + 30 * 2, 20 * 5 + 30 * 1.5)"

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Worked example: the whole-number plans

LoavesCakesFeasible?Profit in pounds
60Yes120
51Yes130
52No, 9 oven hours160 if it were allowed
42Yes140
32Yes120
23Yes130

The cells students fill in are left blank in this preview.

A common mistake

Warning

Many people think you can solve the easy version and round the answer. Rounded plans may be infeasible, and a feasible one may be well short of the best. Search the whole-number plans, or ask a solver that knows the rule.

Ask the solver for whole numbers

Command
docker exec jupyter python3 -c "from scipy.optimize import linprog; r = linprog([-20, -30], A_ub=[[1, 2], [3, 2]], b_ub=[8, 18], integrality=[1, 1]); print(-r.fun, r.x)"

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

linprog can be told which variables are whole

The argument integrality has one number for each variable, and 1 means whole. The command printed 140.0 [4. 2.], the best whole plan that the table found by hand.

The solver does this by solving relaxations again and again. That is why integer problems can take far longer than ordinary ones.

The knapsack problem fits items to a limit

A courier van can carry 5 kg. Parcel 1 weighs 2 kg and pays 3 pounds. Parcel 2 weighs 3 kg and pays 4. Parcel 3 weighs 4 kg and pays 5. Each parcel is taken whole or left behind.

This is the knapsack problem: the most value that fits. Taking parcels 1 and 2 pays 7. Parcel 3 alone pays only 5.

Dynamic programming builds the answer from small cases

List the best value for every amount of room, from 0 kg up to 5 kg. Start with no parcels. Then add one parcel at a time, and ask whether taking it beats the earlier best for that room.

Each step reuses the answers of smaller rooms. This is dynamic programming.

The van in Python

Code: python
weights = [2, 3, 4]
values = [3, 4, 5]
capacity = 5

# best[room] is the largest value that fits in that much room.
best = [0] * (capacity + 1)

for parcel in range(len(weights)):
    for room in range(capacity, weights[parcel] - 1, -1):
        with_parcel = best[room - weights[parcel]] + values[parcel]
        if with_parcel > best[room]:
            best[room] = with_parcel

print(best)
print(best[capacity])
# Change capacity to 6. Work out the answer by hand first.

Worked example: the best value for each room

Room in kgAfter parcel 1After parcels 1 and 2After all three
0000
1000
2333
3344
4345
5377

The cells students fill in are left blank in this preview.

Trying every choice becomes impossible fast

Each parcel is taken or left. So 30 parcels give 2 to the power of 30 choices, and 40 parcels give 2 to the power of 40. Brute force cannot keep up.

Dynamic programming needs only parcels times room steps, but the room can itself be a huge number. In general, whole-number problems have no known fast method for every case.

Count the choices

Command
docker exec jupyter python3 -c "print(2 ** 30, 2 ** 40)"

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Going further

Change the code so the van carries 6 kg, and check the answer against a list of every choice of parcels. Then add a fourth parcel of your own.

Say why the best whole plan for the bakery is not the nearest whole plan to the relaxation.

About this lab and its source: JupyterLab data science lab: pandas, Matplotlib and scikit-learn in the browser

Teaching a class? Set this module for your students with a teacher guide, marked assignments and a lab for every student.