A factory can make two products — call them chairs and tables. Each chair uses 2 board-hours of cutting time and 1 hour of finishing. Each table uses 1 board-hour of cutting and 3 hours of finishing. The factory has 8 cutting hours and 9 finishing hours available per day. Chairs sell at a $30 margin; tables at $50. How many of each should it make?
This is not a puzzle you can answer by intuition. Make only chairs and you run out of cutting time. Make only tables and finishing is the bottleneck. The mix that squeezes out the most profit sits somewhere in the middle, at a specific corner of the region defined by those constraints — and that is exactly what linear programming finds.
59.1 What this chapter helps you do
Symbols to keep handy
These are the bits of notation you'll see a lot. If a line of symbols feels like a fence, read it out loud once, then keep going.
^: objective function — inner product of cost/profit vector with decision vector
A : system of linear inequality constraints
Definitions to keep handy
These are the words we keep coming back to. If one feels slippery, come back here and steady it before you push on.
linear programme (LP): An optimisation problem where the objective and all constraints are linear.
decision variables: The quantities you get to choose.
constraints: The limits your choices must satisfy (resources, requirements, bounds).
feasible region: The set of all choices that satisfy the constraints.
objective function: The thing you’re trying to maximise or minimise (profit, cost, time, etc.).
slack variable: A bookkeeping variable that turns an inequality into an equality by measuring unused resource.
Here is the main move this chapter is making, in plain terms. You do not need to be fast. You just need to keep the thread.
Coming in: You want to maximise or minimise something subject to constraints. The relationships are all linear.
Leaving with: Every LP has a feasible region that is a convex polytope. The optimum is always at a vertex. The simplex method walks the vertices efficiently.
59.2 LP formulation
A linear programme (LP) has three components: decision variables, an objective function, and constraints.
Decision variables. These are the quantities you control. Write them as x_1, x_2, \ldots, x_n — read as “x-sub-one, x-sub-two, up to x-sub-n” — or as the vector \mathbf{x}, read as “bold x.” For the factory problem, x_1 is the number of chairs made per day and x_2 is the number of tables.
Objective function. This is what you are optimising. It is always a linear combination of the decision variables: z = c_1 x_1 + c_2 x_2 +
\cdots + c_n x_n, written compactly as z = \mathbf{c}^\top \mathbf{x}, read “z equals c-transpose times x.” The coefficients \mathbf{c} are your profit margins (for maximisation) or costs (for minimisation).
Constraints. Limits on what the variables can be. Each constraint is a linear inequality: a_{i1} x_1 + a_{i2} x_2 + \cdots \leq b_i. Non-negativity \mathbf{x} \geq \mathbf{0} — read “x is non-negative” — is almost always included separately; you cannot make a negative number of chairs.
Standard form. The simplex method requires equality constraints. Convert each \leq constraint to an equation by adding a slack variables_i \geq 0, read “slack variable i.” Slack absorbs the difference between the left side and the right side:
Now all constraints are equations, all variables are non-negative, and the objective is linear. This is standard form: \text{maximise } \mathbf{c}^\top\mathbf{x} subject to A\mathbf{x} = \mathbf{b}, \mathbf{x} \geq \mathbf{0}.
A second example: diet/resource allocation. A hospital nutrition service needs to provide at least 60 g of protein, at least 800 mg of calcium, and at most 2000 kcal per patient per day from two food types. Food A costs $1.20 per unit, food B costs $0.80 per unit. Formulate the LP to minimise daily cost.
For \geq constraints, introduce surplus variabless_i \geq 0: 15x_1 + 10x_2 - s_1 = 60. The minus sign makes s_i the excess above the minimum. This means a surplus variable records how far you are above a required minimum, rather than how much unused capacity remains below a maximum.
Note that the diet problem’s standard form, with its surplus variables and \geq constraints, is not directly ready for the simplex method as presented below. With surplus variables, the origin is no longer a feasible starting point (it violates the minimum requirements), and finding an initial basic feasible solution requires an additional technique — artificial variables — that is covered in more advanced treatments. The simplex worked example uses only \leq constraints to keep the starting point clear.
59.3 Graphical method
For two decision variables you can draw the feasible region — the set of all (x_1, x_2) satisfying every constraint — and read off the optimum. More importantly, you can see the geometry that the simplex method will exploit algebraically when there are too many variables to draw.
For the factory problem, plot each constraint boundary:
2x_1 + x_2 = 8: line through (4, 0) and (0, 8). Feasible side: below/left.
x_1 + 3x_2 = 9: line through (9, 0) and (0, 3). Feasible side: below/left.
x_1, x_2 \geq 0: first quadrant only.
The feasible region is a convex polygon — a quadrilateral with four vertices: (0,0), (4,0), (3,2), and (0,3).
Corner-point theorem. If an LP has an optimal solution, there is an optimal solution at a vertex (corner point) of the feasible region. This is the foundational geometric fact: the objective function z = 30x_1 + 50x_2 is a family of parallel lines (iso-profit lines). As you push the line in the direction of increasing profit, the last point of contact with the feasible region is a vertex.
Evaluate the objective at each vertex:
Vertex
z = 30x_1 + 50x_2
(0, 0)
0
(4, 0)
120
(3, 2)
190
(0, 3)
150
Maximum profit is $190 per day, achieved by making 3 chairs and 2 tables.
The intersection point (3, 2) comes from solving 2x_1 + x_2 = 8 and x_1 + 3x_2 = 9 simultaneously. Subtract twice the second from the first: -5x_2 = -10, so x_2 = 2, x_1 = 3.
Why the optimum is always at a vertex
The feasible region of an LP is a convex polytope — any two feasible points can be connected by a straight line that stays entirely inside the region. The objective function is linear, so it increases at a constant rate in one direction. If the optimum were in the interior, you could always move a little further in the improving direction and still stay feasible, reaching a higher objective value. The only place where you genuinely cannot improve further is a boundary. And on a boundary, you can keep moving along the edge until you hit a vertex. This is why the simplex method only needs to visit vertices.
Use the sliders to watch the feasible region change and see the optimum stay locked to a corner.
Code
{const W =700, H =420;const xMax =8;const yMax =5;let b1 =8;let b2 =9;functionsvgEl(tag) {returndocument.createElementNS("http://www.w3.org/2000/svg", tag); }functionyFromC1(x) {return b1 -2* x; }functionyFromC2(x) {return (b2 - x) /3; }const container =document.createElement("div"); container.style.cssText="max-width: 760px; margin: 1rem 0 1.25rem 0; font-family: inherit;";const controls =document.createElement("div"); controls.style.cssText="display:grid; grid-template-columns:auto 1fr auto; gap:0.5rem 0.75rem; align-items:center; margin-bottom:0.7rem;";functionsliderRow(labelText, min, max, value) {const label =document.createElement("label"); label.textContent= labelText;const input =document.createElement("input"); input.type="range"; input.min=String(min); input.max=String(max); input.step="0.5"; input.value=String(value);const out =document.createElement("span"); out.style.cssText="font-variant-numeric: tabular-nums; min-width:3.4rem;"; controls.append(label, input, out);return { input, out }; }const c1 =sliderRow("Cutting-hours RHS",5,11, b1);const c2 =sliderRow("Finishing-hours RHS",6,12, b2);const readout =document.createElement("div"); readout.style.cssText="display:flex; gap:0.75rem; flex-wrap:wrap; margin-bottom:0.65rem;";functioncard(label) {const box =document.createElement("div"); box.style.cssText="background:#f8fafc; border:1px solid #e5e7eb; border-radius:8px; padding:0.5rem 0.65rem;";const k =document.createElement("div"); k.style.cssText="font-size:0.76rem; color:#6b7280;"; k.textContent= label;const v =document.createElement("div"); v.style.cssText="font-size:0.96rem; font-variant-numeric: tabular-nums;"; box.append(k, v); readout.appendChild(box);return v; }const vertexBox =card("Optimal vertex");const valueBox =card("Objective z");const bindBox =card("Active constraints");const svg =svgEl("svg"); svg.setAttribute("viewBox",`0 0 ${W}${H}`); svg.setAttribute("width","100%"); svg.style.cssText="display:block; background:white; border:1px solid #e5e7eb; border-radius:10px;";const caption =document.createElement("p"); caption.style.cssText="font-size:0.9rem; color:#4b5563; margin:0.55rem 0 0 0;";const left =60, right =24, top =26, bottom =42;const plotW = W - left - right, plotH = H - top - bottom;const sx = x => left + plotW * x / xMax;const sy = y => top + plotH * (1- y / yMax);functionfeasible(pt) {const [x, y] = pt;return x >=-1e-9&& y >=-1e-9&&2* x + y <= b1 +1e-9&& x +3* y <= b2 +1e-9; }functionsortPoly(pts) {const cx = pts.reduce((s, p) => s + p[0],0) / pts.length;const cy = pts.reduce((s, p) => s + p[1],0) / pts.length;return pts.slice().sort((a, b) =>Math.atan2(a[1] - cy, a[0] - cx) -Math.atan2(b[1] - cy, b[0] - cx)); }functionredraw() { b1 =parseFloat(c1.input.value); b2 =parseFloat(c2.input.value); c1.out.textContent= b1.toFixed(1); c2.out.textContent= b2.toFixed(1);while (svg.firstChild) svg.removeChild(svg.firstChild);for (let x =0; x <= xMax; x +=1) {const line =svgEl("line"); line.setAttribute("x1",sx(x)); line.setAttribute("x2",sx(x)); line.setAttribute("y1",sy(0)); line.setAttribute("y2",sy(yMax)); line.setAttribute("stroke","#f3f4f6"); svg.appendChild(line); }for (let y =0; y <= yMax; y +=1) {const line =svgEl("line"); line.setAttribute("x1",sx(0)); line.setAttribute("x2",sx(xMax)); line.setAttribute("y1",sy(y)); line.setAttribute("y2",sy(y)); line.setAttribute("stroke","#f3f4f6"); svg.appendChild(line); }const axX =svgEl("line"); axX.setAttribute("x1",sx(0)); axX.setAttribute("x2",sx(xMax)); axX.setAttribute("y1",sy(0)); axX.setAttribute("y2",sy(0)); axX.setAttribute("stroke","#6b7280"); axX.setAttribute("stroke-width","1.5"); svg.appendChild(axX);const axY =svgEl("line"); axY.setAttribute("x1",sx(0)); axY.setAttribute("x2",sx(0)); axY.setAttribute("y1",sy(0)); axY.setAttribute("y2",sy(yMax)); axY.setAttribute("stroke","#6b7280"); axY.setAttribute("stroke-width","1.5"); svg.appendChild(axY);const intersection = [(3* b1 - b2) /5, (2* b2 - b1) /5];const candidates = [[0,0], [b1 /2,0], [0, b2 /3], intersection];const feasiblePts = candidates.filter(feasible);const poly =sortPoly(feasiblePts);const polyNode =svgEl("polygon"); polyNode.setAttribute("points", poly.map(([x, y]) =>`${sx(x)},${sy(y)}`).join(" ")); polyNode.setAttribute("fill","rgba(20,184,166,0.22)"); polyNode.setAttribute("stroke","#0f766e"); polyNode.setAttribute("stroke-width","2"); svg.appendChild(polyNode); [ { fn: yFromC1,color:"#2563eb",x2:Math.min(xMax, b1 /2),label:"2x₁ + x₂ = RHS" }, { fn: yFromC2,color:"#f59e0b",x2:Math.min(xMax, b2),label:"x₁ + 3x₂ = RHS" } ].forEach(item => {const line =svgEl("line"); line.setAttribute("x1",sx(0)); line.setAttribute("y1",sy(item.fn(0))); line.setAttribute("x2",sx(item.x2)); line.setAttribute("y2",sy(item.fn(item.x2))); line.setAttribute("stroke", item.color); line.setAttribute("stroke-width","2.5"); svg.appendChild(line);const text =svgEl("text"); text.setAttribute("x",sx(item.x2) -8); text.setAttribute("y",sy(item.fn(item.x2)) -8); text.setAttribute("text-anchor","end"); text.setAttribute("font-size","12"); text.setAttribute("fill", item.color); text.textContent= item.label; svg.appendChild(text); });let best = feasiblePts[0];let bestZ =-Infinity; feasiblePts.forEach(pt => {const z =30* pt[0] +50* pt[1];if (z > bestZ) { best = pt; bestZ = z; } });const zLine =svgEl("line"); zLine.setAttribute("x1",sx(0)); zLine.setAttribute("y1",sy(bestZ /50)); zLine.setAttribute("x2",sx(bestZ /30)); zLine.setAttribute("y2",sy(0)); zLine.setAttribute("stroke","#7c3aed"); zLine.setAttribute("stroke-width","2.2"); zLine.setAttribute("stroke-dasharray","7 5"); svg.appendChild(zLine); feasiblePts.forEach(([x, y]) => {const dot =svgEl("circle"); dot.setAttribute("cx",sx(x)); dot.setAttribute("cy",sy(y)); dot.setAttribute("r", x === best[0] && y === best[1] ?"6.5":"4"); dot.setAttribute("fill", x === best[0] && y === best[1] ?"#7c3aed":"#0f766e"); svg.appendChild(dot); });const label =svgEl("text"); label.setAttribute("x",sx(best[0]) +10); label.setAttribute("y",sy(best[1]) -10); label.setAttribute("font-size","12"); label.setAttribute("fill","#6d28d9"); label.textContent=`best = (${best[0].toFixed(2)}, ${best[1].toFixed(2)})`; svg.appendChild(label); vertexBox.textContent=`(${best[0].toFixed(2)}, ${best[1].toFixed(2)})`; valueBox.textContent=`${bestZ.toFixed(1)}`;const active = [];if (Math.abs(2* best[0] + best[1] - b1) <1e-6) active.push("cutting");if (Math.abs(best[0] +3* best[1] - b2) <1e-6) active.push("finishing");if (Math.abs(best[0]) <1e-6) active.push("x₁ = 0");if (Math.abs(best[1]) <1e-6) active.push("x₂ = 0"); bindBox.textContent= active.join(", "); caption.textContent="The shaded polygon is the feasible region. The dashed purple line is the objective contour at the best value. Sliding the right-hand sides changes the polygon, but the optimum still occurs at a vertex."; } [c1.input, c2.input].forEach(el => el.addEventListener("input", redraw)); container.append(controls, readout, svg, caption);redraw();return container;}
What the graphical method shows directly is what the simplex method does systematically: start at one vertex (here, the origin), check whether any neighbour is better, move to the best neighbour, and repeat. The graphical picture makes the logic self-evident. The simplex method turns that logic into algebra so it can work in a hundred dimensions instead of two.
59.4 Simplex method
The graphical method breaks down beyond two variables. The simplex method does the same thing — walk from vertex to vertex, improving the objective each step — algebraically, using the tableau.
Setup: slack variables and initial tableau. Return to the factory problem in standard form with x_1, x_2 (original variables) and s_1, s_2 (slacks):
The basic feasible solution (BFS) at the origin: set non-basic variables x_1 = x_2 = 0, read off basic variables s_1 = 8, s_2 = 9, z = 0. This is the vertex (0, 0).
Pivot selection. Look at the objective row (bottom row). The most negative coefficient is -50 in the x_2 column: x_2 enters the basis. The column x_2 is the entering column.
To choose the leaving variable, compute the minimum ratio: \text{RHS} /
\text{entering-column coefficient} for rows with positive pivot-column entries only. Rows with zero or negative entries in the pivot column are skipped: a zero entry means increasing x_2 has no effect on that constraint, and a negative entry means the constraint becomes easier to satisfy as x_2 increases — neither constrains the move.
\frac{8}{1} = 8 \qquad \frac{9}{3} = 3
Minimum ratio is 3, so s_2 leaves. The pivot element is the entry in row s_2, column x_2: that is 3.
Pivot operation. Divide the s_2 row by 3 to make the pivot element 1:
All objective-row coefficients are non-negative. Termination condition met. Optimal solution: x_1 = 3, x_2 = 2, z = 190. This means there is no adjacent basic feasible solution that can improve the objective any further, so the current vertex is optimal.
This matches the graphical answer exactly.
Why simplex terminates
Each pivot strictly improves the objective (assuming no degeneracy — no zero RHS values). There are finitely many vertices. So the method must reach an optimum in a finite number of steps. In practice, simplex visits far fewer vertices than the total count even for large LPs.
Use the stepper to see how simplex moves from one basic feasible solution to the next by pivoting on an entering column and a leaving row.
Code
{const steps = [ {title:"Initial tableau",entering:"x2",leaving:"s2",bfs:"x1 = 0, x2 = 0, z = 0",rows: [ ["s1","2","1","1","0","8"], ["s2","1","3","0","1","9"], ["z","-30","-50","0","0","0"] ] }, {title:"After pivot 1",entering:"x1",leaving:"s1",bfs:"x2 = 3, x1 = 0, z = 150",rows: [ ["s1","5/3","0","1","-1/3","5"], ["x2","1/3","1","0","1/3","3"], ["z","-40/3","0","0","50/3","150"] ] }, {title:"Final tableau",entering:"none",leaving:"none",bfs:"x1 = 3, x2 = 2, z = 190",rows: [ ["x1","1","0","3/5","-1/5","3"], ["x2","0","1","-1/5","2/5","2"], ["z","0","0","8","14","190"] ] } ];const container =document.createElement("div"); container.style.cssText="max-width:760px; margin:1rem 0 1.25rem 0; font-family:inherit;";const actions =document.createElement("div"); actions.style.cssText="display:flex; gap:0.6rem; align-items:center; margin-bottom:0.7rem;";const prevBtn =document.createElement("button"); prevBtn.textContent="Previous";const nextBtn =document.createElement("button"); nextBtn.textContent="Next pivot";const stage =document.createElement("strong"); actions.append(prevBtn, nextBtn, stage);const summary =document.createElement("div"); summary.style.cssText="display:flex; gap:1rem; flex-wrap:wrap; margin-bottom:0.7rem; color:#334155;";const enterBox =document.createElement("span");const leaveBox =document.createElement("span");const bfsBox =document.createElement("span"); summary.append(enterBox, leaveBox, bfsBox);const table =document.createElement("table"); table.style.cssText="border-collapse:collapse; width:100%; max-width:700px; font-size:0.95rem;";const caption =document.createElement("p"); caption.style.cssText="margin:0.65rem 0 0 0; color:#475569;";let idx =0;functioncell(text, header =false) {const el =document.createElement(header ?"th":"td"); el.textContent= text; el.style.cssText="border:1px solid #cbd5e1; padding:0.45rem 0.55rem; text-align:center;";return el; }functionredraw() {const step = steps[idx]; stage.textContent= step.title; enterBox.textContent=`entering variable: ${step.entering}`; leaveBox.textContent=`leaving variable: ${step.leaving}`; bfsBox.textContent=`current BFS: ${step.bfs}`; table.replaceChildren();const thead =document.createElement("thead");const hr =document.createElement("tr"); ["Basic","x1","x2","s1","s2","RHS"].forEach(h => hr.appendChild(cell(h,true))); thead.appendChild(hr); table.appendChild(thead);const tbody =document.createElement("tbody"); step.rows.forEach(row => {const tr =document.createElement("tr"); row.forEach((val, j) => {const td =cell(val);if (j ===0&& val === step.leaving) td.style.background="#fde68a";if (j >0&& ["x1","x2","s1","s2"][j -1] === step.entering) td.style.background="#bfdbfe";if (j ===0&& val ==="z") td.style.fontWeight="700"; tr.appendChild(td); }); tbody.appendChild(tr); }); table.appendChild(tbody); caption.textContent= idx < steps.length-1?"Blue cells mark the entering column and amber marks the leaving row. The next pivot moves the basis to an adjacent vertex with a better objective value.":"At the final tableau all objective-row coefficients are non-negative, so no adjacent basic feasible solution can improve the objective."; } prevBtn.addEventListener("click", () => { idx =Math.max(0, idx -1);redraw(); }); nextBtn.addEventListener("click", () => { idx =Math.min(steps.length-1, idx +1);redraw(); }); container.append(actions, summary, table, caption);redraw();return container;}
59.5 Duality
Every LP — the primal — has a companion LP called the dual. The dual is not just a mathematical curiosity; it has direct economic meaning and provides a powerful optimality check.
Constructing the dual. Start from the primal in standard inequality form:
The roles reverse: primal constraints become dual variables, primal variables become dual constraints, primal objective coefficients become dual RHS values, and primal RHS values become dual objective coefficients. The maximisation becomes minimisation.
Dual of the factory problem. Primal: maximise 30x_1 + 50x_2 subject to 2x_1 + x_2 \leq 8, x_1 + 3x_2 \leq 9, x_1, x_2 \geq 0.
Let y_1, y_2 be the dual variables (one per primal constraint):
Proof. The dual constraint says A^\top\mathbf{y} \geq \mathbf{c}, meaning each component satisfies (A^\top\mathbf{y})_j \geq c_j. Since x_j \geq 0, multiplying through and summing over all j gives \mathbf{c}^\top \mathbf{x} \leq (A^\top\mathbf{y})^\top \mathbf{x} = \mathbf{y}^\top A\mathbf{x}. The primal constraint A\mathbf{x} \leq \mathbf{b} combined with \mathbf{y} \geq \mathbf{0} then gives \mathbf{y}^\top A\mathbf{x} \leq \mathbf{y}^\top \mathbf{b} = \mathbf{b}^\top\mathbf{y}. Chaining both inequalities: \mathbf{c}^\top \mathbf{x} \leq \mathbf{b}^\top\mathbf{y}. \square
The strong duality theorem (proved via simplex theory) says the inequality is tight at optimality: primal and dual optimal values are equal.
Economic interpretation: shadow prices. The optimal dual variables have a direct meaning. y_1 is the shadow price of the first constraint (cutting hours): it tells you how much the optimal objective increases if you add one unit of that resource. At optimality, y_1 = 8 and y_2 = 14 — each additional cutting hour is worth $8, each additional finishing hour is worth $14. You can verify: 8y_1 + 9y_2 = 8 \times 8 + 9 \times 14 = 64 + 126 = 190. Dual optimal matches primal optimal — strong duality holds. This means the dual variables are not abstract extras: they price the scarce resources in exactly the units of the original objective.
Complementary slackness. At optimality, either a primal constraint is tight (active, A_i \mathbf{x} = b_i) or the corresponding dual variable is zero (y_i = 0); and either a dual constraint is tight or the corresponding primal variable is zero. Formally: y_i(b_i - A_i\mathbf{x}) = 0 and (A^\top\mathbf{y} - \mathbf{c})_j x_j = 0 for all i, j. This gives you a system of equations to verify optimality without running the full simplex.
59.6 Sensitivity analysis
The optimal basis found by simplex remains optimal as long as the problem data stays within certain ranges. If the profit margin on x_1 changes from 30 to 30 + \Delta, the basis \{x_1, x_2\} stays optimal while the objective-row coefficient of s_1 (currently 8) remains non-negative and the coefficient of s_2 (currently 14) remains non-negative. The x_1 basic row is [1, 0, 3/5, -1/5, 3], so changing c_1 by \Delta shifts the s_1 coefficient to 8 - \tfrac{3}{5}\Delta and the s_2 coefficient to 14 + \tfrac{1}{5}\Delta. Requiring both to remain non-negative: 8 - \tfrac{3}{5}\Delta \geq 0 \Rightarrow \Delta \leq \tfrac{40}{3} and 14 + \tfrac{1}{5}\Delta \geq 0 \Rightarrow \Delta \geq -70. The basis is stable for -70 \leq \Delta \leq \tfrac{40}{3}, i.e., c_1 \in [-40, 43\tfrac{1}{3}]. Similarly, if the RHS b_1 = 8 changes, the solution coordinates shift but the same basis remains feasible as long as all basic variables stay non-negative — the range can be read from the current tableau’s column for s_1. This analysis is done without re-running simplex.
59.7 Where this goes
Chapter 2 of this section develops network flow problems — shortest path, maximum flow, minimum cost flow — all of which can be formulated as LPs with special structure (network matrices) that allows faster algorithms than general simplex. Network LPs arise everywhere: logistics, circuit analysis, pipeline routing.
The broader field of integer programming (mixed-integer LP) handles the case where some or all variables must be integers — scheduling problems, facility location, combinatorial optimisation. Branch-and-bound solves these by solving a sequence of LP relaxations. The LP theory in this chapter is the computational core of those methods.
Where LP shows up
A refinery blends crude feedstocks to meet fuel-grade specifications at minimum cost — an LP with 50–500 variables, solved daily.
An airline schedules crews to cover flights while meeting duty-time regulations — an integer LP with millions of variables in practice.
A fund manager maximises expected portfolio return subject to risk limits and position constraints — LP or quadratic programming depending on the risk model.
A nutritionist formulates minimum-cost meal plans meeting dietary requirements — the diet problem, which drove early LP development.
A chip manufacturer allocates fab capacity across product lines to maximise margin — LP with supply chain constraints.
59.8 Exercises
These are puzzles. Each has a clean optimal solution. The interesting work is in the formulation — translating the situation into variables, objective, and constraints — and in executing the simplex steps carefully.
Exercise 1. A workshop makes two products, shelves and benches. Each shelf uses 3 kg of timber and 1 hour of labour. Each bench uses 2 kg of timber and 4 hours of labour. The workshop has 24 kg of timber and 16 hours of labour available per week. Profit is $20 per shelf and $45 per bench. Formulate the LP to maximise weekly profit.
Exercise 2. Solve the LP from Exercise 1 graphically. Plot both constraint boundaries, shade the feasible region, evaluate the objective at every vertex, and identify the optimal solution.
Exercise 3. Convert the LP from Exercise 1 to standard form by introducing slack variables. Write out the full system of equality constraints and identify the initial basic feasible solution (the origin vertex).
Exercise 4. Solve the following LP using the simplex tableau method. Show all pivots.
Exercise 5. Write the dual of the LP in Exercise 4. Then use complementary slackness to find the dual optimal solution (y_1, y_2). Verify strong duality by checking that primal and dual objective values agree at optimality.
Exercise 6. In Exercise 4, the optimal basis has x_1 basic (value 3) and x_2 basic (value 3/2). By how much can the objective coefficient of x_1 (currently 5) change before the current basis is no longer optimal? Give the range.