Algorithm Design and Analysis

Approximation Algorithms

On this page

Approximation Algorithms

Seven test suites, three runners

A team's continuous-integration pipeline runs seven test suites that take 6, 8, 4, 5, 2, 3 and 9 minutes. There are three identical runners, each runs one suite at a time, and a suite can't be split. The team wants every suite finished as early as possible: it wants to minimise the makespan, the time at which the last runner finishes.

6 0 8 1 4 2 5 3 2 4 3 5 9 6
The seven suites J, in the order they arrive (minutes). Three runners; the total is 37 minutes of work.

In general there are n jobs with sizes p1,…,pn and m identical machines. A schedule assigns each job to one machine, a machine's load is the sum of its jobs' sizes, and the makespan is the largest load.

from itertools import product

J, m = [6, 8, 4, 5, 2, 3, 9], 3

def makespan(jobs, machine, m):
    loads = [0] * m
    for p, i in zip(jobs, machine):
        loads[i] += p
    return max(loads)

def best_makespan(jobs, m):
    """Try all m ** n schedules (small inputs only)."""
    return min(makespan(jobs, s, m) for s in product(range(m), repeat=len(jobs)))

assert sum(J) == 37 and 3 ** 7 == 2187
assert best_makespan(J, m) == 13

Why not just find the best schedule?

Trying every schedule is mn work: 2,187 schedules here, fine, but 320=3,486,784,401 for twenty suites, and a real pipeline has hundreds. No shortcut is known. Minimum makespan is NP-hard already for two machines, and so are the other two problems in this module, minimum-weight vertex cover and the travelling salesman tour. This course takes NP-hardness as given, without the reductions: it means that no polynomial-time algorithm solves the problem exactly on every instance unless P = NP.

(Two refinements are worth knowing. For a fixed number of machines, a dynamic program over loads runs in time polynomial in n and in the total size ∑pj, which is pseudo-polynomial, as in the knapsack of Dynamic Programming I. And the hardness is about the worst instance: many instances are easy.)

So this module gives up exactness, and keeps a proof.

The model: a factor on every instance

Definition

For a minimisation problem and a number α≥1, an α-approximation algorithm runs in time polynomial in the input size and, on every instance I, returns a feasible solution whose cost satisfies ALG(I)≤α·OPT(I).

The ratio is a worst-case guarantee over all instances, not typical accuracy. A thousand instances where an algorithm is within 1% prove nothing, and one instance above α refutes the claim.

But how can we prove ALG≤α·OPT when OPT is exactly the number we can't compute? Every proof in this module has the same shape. Find a quantity L that we can reason about and that no solution can beat, so L≤OPT, then prove ALG≤αL. For makespan, two lower bounds hold for every schedule. Some machine runs the largest job, and the busiest machine carries at least the average load:

OPT≥maxjpj,OPT≥1m∑jpj.

For the seven suites that gives max(9,⌈37/3⌉)=13 (loads are whole numbers, so we may round up), and the search above shows that 13 is achievable. On other instances the lower bound may be strictly below OPT; the proofs only need L≤OPT.

Greedy on the seven suites

Greedy list scheduling takes the jobs in the order given and puts each one on a machine whose load is currently smallest (the lowest-numbered one on a tie). It never looks ahead, so it could even run while suites are still arriving.

+6 +8 +4 +5 +2 +3 +9 runner 0 runner 1 runner 2 6 0 0 6 8 0 6 8 4 6 8 9 8 8 9 11 8 9 11 17 9
Greedy list scheduling of J: one row per suite, in arrival order, showing the three loads after it is placed; the shaded cell is the runner that took it (the least loaded, lowest number on a tie). The last suite, 9 minutes, starts at load 8 on runner 1 and ends at 17, against a lower bound (and optimum) of 13.
Predict: the 9-minute suite started at time 8. Could it have started later than the average load of the other six suites?

No. When it was placed, its runner was the least loaded, so all three runners were at load 8 or more, from the other six suites only. Three runners at 8 or more need at least 24 minutes of those suites, and they have 37−9=28. In general the start is at most (∑kpk−pj)/m, which is 28/3≈9.33 here.

The block below checks that the trace follows the rule. It doesn't schedule anything: it takes the runners the figure names and confirms that each one was least loaded at the time.

def follows_greedy(jobs, machine, m):
    loads = [0] * m
    for p, i in zip(jobs, machine):
        if loads[i] != min(loads) or loads.index(min(loads)) != i:
            return False
        loads[i] += p
    return True

greedy_J = [0, 1, 2, 2, 0, 0, 1]
assert follows_greedy(J, greedy_J, m) and makespan(J, greedy_J, m) == 17
assert 3 * 8 <= 37 - 9                      # the 9 started at load 8

Why greedy is within 2−1/m

Theorem (Graham, 1966)

For every m≥1 and every list of jobs with positive sizes, in any order, greedy list scheduling returns a makespan of at most (2−1/m)·OPT.

Proof. Let machine i attain the makespan and let job j be the last job placed on it, starting at load s, so ALG=s+pj. When j was placed, machine i was least loaded, so every machine had load at least s, and those loads consist of jobs other than j. Hence

ms≤∑kpk−pj.

Dividing by m and adding pj:

ALG≤1m∑kpk+(1−1m)pj.

The first term is at most OPT (the average-load bound) and pj≤OPT (the largest-job bound), so ALG≤(2−1/m)OPT. ◻

The step not to skip is leaving pj out of the average. Put it back in and the same argument only gives 2OPT. The −1/m is real, because the bound is tight: for every m, some instance meets it exactly. Give greedy m(m−1) jobs of size 1 and then one job of size m. The unit jobs spread evenly, load m−1 everywhere, and the big job lands on top: makespan 2m−1. The optimum puts the big job alone and the unit jobs m to a machine elsewhere: makespan m. The ratio is (2m−1)/m=2−1/m.

tight = [1] * 6 + [3]                       # m = 3: six unit jobs, then a 3
assert follows_greedy(tight, [0, 1, 2, 0, 1, 2, 0], m)
assert makespan(tight, [0, 1, 2, 0, 1, 2, 0], m) == 5
assert best_makespan(tight, m) == 3         # 5/3 = 2 - 1/3

Sort first: largest processing time

The tight instance hurts greedy because the big job comes last. LPT (largest processing time first) sorts the jobs by decreasing size, then runs greedy. On the seven suites it places 9, 8, 6, 5, 4, 3, 2 and ends with loads 12, 12 and 13: makespan 13, optimal here.

Theorem (Graham, 1969)

For every m≥1 and every list of jobs with positive sizes, LPT returns a makespan of at most (43−13m)OPT, and the bound is tight for every m.

Sketch. Let j be the last job on the busiest machine. Jobs placed after j don't change the makespan and removing them can't raise OPT, so assume j is placed last, which in LPT order makes it the smallest job. If pj≤OPT/3, the greedy argument gives

ALG≤OPT+pj≤43OPT.

Otherwise every job is larger than OPT/3, so the optimum has at most two jobs per machine, and a case analysis shows LPT is optimal then. Skipped: that case analysis and the refinement to the exact −1/(3m).

LPT is not optimal in general. On three machines, [5, 5, 4, 4, 3, 3, 3] is its tight case:

lpt_J = [2, 1, 1, 2, 2, 0, 0]               # runner of each suite of J, in J's order
order = sorted(range(7), key=lambda k: -J[k])
assert follows_greedy([J[k] for k in order], [lpt_J[k] for k in order], m)
assert makespan(J, lpt_J, m) == 13

trap = [5, 5, 4, 4, 3, 3, 3]
assert follows_greedy(trap, [0, 1, 2, 2, 0, 1, 0], m)
assert makespan(trap, [0, 1, 2, 2, 0, 1, 0], m) == 11
assert best_makespan(trap, m) == 9          # {5,4} {5,4} {3,3,3}: 11/9 = 4/3 - 1/9

For every fixed ε>0 there is even a (1+ε)-approximation whose running time is polynomial in n (Hochbaum and Shmoys, 1987), cited here, not proved; its exponent grows as ε shrinks.

Vertex cover: round the linear program

A different problem. A graph G=(V,E) has a weight wv>0 on each vertex, and a vertex cover is a set S of vertices that touches every edge. Find one of minimum total weight.

0 1 2 3 4 5 6
Graph H: a triangle 0 1 2, the edge 2-3, and a 4-cycle 3 4 5 6 with the chord 4-6. Weights w = 2, 4, 1, 4, 7, 5, 3 for vertices 0 to 6. Green: the cheapest cover {0, 2, 4, 6}, weight 13.

Write the problem as an integer program with a 0/1 variable xv for "v is in the cover", then relax it (Linear Programming) by allowing fractions:

minimise∑vwvxv

subject to xu+xv≥1 for every edge uv, and 0≤xv≤1 for every vertex.

Every cover is a feasible point of this LP, so the LP's minimum is a lower bound: LP≤OPT. And the LP can be solved in time polynomial in the input's bit length (Linear Programming). The LP optimum is usually fractional. Here it is x=(1,0,1,12,12,12,12), of value 12.5.

A classical fact, stated here without proof, makes that easy to check on small graphs: the vertex-cover LP always has an optimal solution with every xv∈{0,12,1} (every corner of its polytope is of this form; Nemhauser and Trotter, 1974). So the block searches those points, written as y=2x:

from fractions import Fraction

H = [(0, 1), (1, 2), (2, 0), (2, 3), (3, 4), (4, 5), (5, 6), (6, 3), (4, 6)]
w = [2, 4, 1, 4, 7, 5, 3]

def lp_half(n, edges, w):
    """(LP value, x) over x in {0, 1/2, 1}^n: exact, by the half-integrality fact."""
    best = min((sum(wi * yi for wi, yi in zip(w, y)), y)
               for y in product((0, 1, 2), repeat=n)
               if all(y[u] + y[v] >= 2 for u, v in edges))
    return Fraction(best[0], 2), [Fraction(yi, 2) for yi in best[1]]

def cheapest_cover(n, edges, w):
    covers = (S for S in product((0, 1), repeat=n) if all(S[u] or S[v] for u, v in edges))
    return min(sum(wi for wi, s in zip(w, S) if s) for S in covers)

lp, x = lp_half(7, H, w)
assert lp == Fraction(25, 2) and x == [1, 0, 1, Fraction(1, 2), Fraction(1, 2), Fraction(1, 2), Fraction(1, 2)]
assert cheapest_cover(7, H, w) == 13

Rounding. Keep every vertex with xv≥12.

Theorem (LP rounding)

For every graph with nonnegative vertex weights, the set {v:xv≥12} of an optimal LP solution x is a vertex cover of weight at most 2LP≤2OPT.

Proof. Feasibility: every edge has xu+xv≥1, so one of its ends has x≥12 and is kept. Cost: each kept vertex has 1≤2xv, so wv≤2wvxv, and summing over the kept vertices gives at most 2∑vwvxv=2LP. ◻

keep = {v for v in range(7) if x[v] >= Fraction(1, 2)}
assert keep == {0, 2, 3, 4, 5, 6} and sum(w[v] for v in keep) == 22 <= 2 * lp

Rounding keeps weight 22 against the optimum's 13: a ratio of about 1.69, within 2. The lower bound here is 12.5, and every edge of the 4-cycle and chord gets exactly 12+12.

Predict: rounding at 0.6 instead of 1/2 would keep fewer vertices. What goes wrong?

On H the vertices 3 and 4 both have x=12, so neither is kept, and the edge 3–4 is covered by nobody. The threshold 12 is exactly what xu+xv≥1 guarantees for one end; any higher threshold can lose feasibility.

No polynomial-time algorithm with ratio 2−ε, for a constant ε>0, is known for vertex cover. Under the Unique Games Conjecture none exists (Khot and Regev, 2008); that is a conditional result, cited here.

Without weights: a matching is a lower bound

When every weight is 1, there is a cheaper route that needs no LP. Scan the edges in any order. Whenever an edge has neither end taken yet, take both ends.

Theorem (maximal matching)

For every graph and every scan order, the taken vertices form a vertex cover with at most twice as many vertices as a smallest one.

Proof. Every edge is covered, since when it was scanned an end was already taken or both were taken then. The edges that triggered a "take both" share no vertex (their ends were untaken when they were chosen), so they form a matching M. Any cover needs a separate vertex for each edge of M, so OPT≥|M|, and the algorithm took 2|M|. ◻

The lower bound L is now |M|, the size of the matching the scan built. On H, scanning in the listed order picks the edges 0–1, 2–3 and 4–5:

M = [(0, 1), (2, 3), (4, 5)]
ends = [v for e in M for v in e]
assert len(set(ends)) == 6                                   # a matching
assert all(u in ends or v in ends for u, v in H)             # nothing left to add: a cover
assert cheapest_cover(7, H, [1] * 7) == 4                     # 6 <= 2 * 4

The matching bound ignores weights, and with weights it fails: a cheap vertex matched to an expensive one drags the expensive one in. So the scan is a 2-approximation for the unweighted problem only; with weights, use LP rounding.

Metric travelling salesman: double a tree

The last problem: visit n points and return to the start, as cheaply as possible. The distance d must be a metric: d(x,x)=0, symmetric, and obeying the triangle inequality d(x,z)≤d(x,y)+d(y,z). Here six points on a street grid use the Manhattan distance |x1−x2|+|y1−y2|, which is a metric.

Tree doubling. Build a minimum spanning tree T (Prim's or Kruskal's algorithm, from a data-structures course). Walk around it depth first, which crosses every tree edge twice, and list each point the first time the walk reaches it (a preorder). Return to the start.

2 2 5 5 4 0 1 2 3 4 5
Points 0 (2,5), 1 (3,8), 2 (8,8), 3 (5,3), 4 (9,3) and 5 (3,6) under the Manhattan distance. Blue: a minimum spanning tree T, weight 2 + 2 + 5 + 5 + 4 = 18.
5 4 9 2 5 9 0 1 2 3 4 5
The preorder tour 0 3 4 5 1 2 and back to 0: length 5 + 4 + 9 + 2 + 5 + 9 = 34, within 2 w(T) = 36. The hops 4 to 5 and 2 to 0 are the shortcuts past points already visited.

Theorem (tree doubling)

For every set of points under a metric, the preorder tour of a minimum spanning tree has length at most 2w(T)≤2OPT.

Proof. Delete one edge from an optimal tour: what is left is a path through every point, which is a spanning tree, so w(T)≤OPT. The full walk around T has length 2w(T). The preorder tour visits the points in the walk's order but jumps straight past points already seen. Each jump replaces a stretch of the walk by one direct hop, and by the triangle inequality (applied along the stretch) the hop is no longer than the stretch. ◻

from itertools import combinations, permutations

P = [(2, 5), (3, 8), (8, 8), (5, 3), (9, 3), (3, 6)]
d = lambda a, b: abs(P[a][0] - P[b][0]) + abs(P[a][1] - P[b][1])
length = lambda t: sum(d(t[k], t[(k + 1) % len(t)]) for k in range(len(t)))

def connected(edges, n):
    seen, todo = {0}, [0]
    while todo:
        a = todo.pop()
        for b in {v for e in edges if a in e for v in e} - seen:
            seen.add(b)
            todo.append(b)
    return len(seen) == n

# A stand-in for Prim's algorithm: the lightest of all 3,003 sets of 5 edges that connect the points.
mst_weight = min(sum(d(a, b) for a, b in T)
                 for T in combinations(combinations(range(6), 2), 5) if connected(T, 6))
T = [(0, 5), (5, 1), (1, 2), (0, 3), (3, 4)]
tour = [0, 3, 4, 5, 1, 2]                    # preorder from 0, children in increasing order
assert sum(d(a, b) for a, b in T) == mst_weight == 18
assert length(tour) == 34 <= 2 * mst_weight
assert min(length((0,) + p) for p in permutations(range(1, 6))) == 24     # the optimum

The tour 0, 3, 4, 5, 1, 2 has length 34, within the promised 36; the best tour has length 24 (ratio about 1.42). Christofides' algorithm (1976) adds a minimum-weight perfect matching on the tree's odd-degree vertices instead of doubling every edge and reaches 3/2; a 2021 randomized algorithm (Karlin, Klein and Oveis Gharan) improves 3/2, in expectation, by a constant above 10−36. Both are cited, not proved.

The triangle inequality is not a technicality. Without it, for any constant α, no polynomial-time α-approximation exists for the travelling salesman problem unless P = NP (Sahni and Gonzalez, 1976).

Complexity

All times are worst case in the word-RAM model, with sizes, weights and coordinates that fit in a machine word; n is the number of jobs, vertices or points.

Algorithm Ratio, on every instance Time
greedy list scheduling 2−1/m, tight O(nm), or O(nlogm) with a heap
LPT 4/3 proved; 4/3−1/(3m) stated, tight O(nlogn) with a heap
LP rounding, weighted cover 2 polynomial (the LP), then O(n)
maximal matching, unweighted cover 2 O(n+|E|)
tree doubling, metric TSP 2 O(n2) with array-based Prim

Exact search costs mn schedules, 2n vertex sets or (n−1)!/2 tours. The ratios are proved, except LPT's refinement from 4/3 to 4/3−1/(3m), which is stated; the tight instances show that greedy's and LPT's can't be improved.

Measure the claim

On random small instances generated with seed 451 (200 job lists with m∈{2,3}, at most 7 jobs of size 1 to 20; 200 graphs of at most 8 vertices with weights 1 to 9; 100 sets of at most 7 points on a 10×10 grid), the worst ratios observed against exact brute force were:

Algorithm Worst observed Proved bound
greedy list scheduling 1.412 3/2 for m=2, 5/3 for m=3
LPT 1.083 7/6 for m=2, 11/9 for m=3
LP rounding 2.0 2
maximal matching 2.0 2
tree doubling 1.467 2

These are measurements on these instances, not theorems. Random instances rarely find the worst case: greedy never came near 5/3, and LPT not near 11/9, although the tight instances above meet both bounds exactly. The two cover algorithms did reach 2, on tiny graphs. The matching scan takes both ends of a single edge, where one end suffices. Rounding reached 2 on a triangle with weights 3, 6 and 3, where the LP has several optimal solutions and the search returned x=12 everywhere (value 6): rounding keeps all three vertices, weight 12, while the cover {0,2} costs 6. Worst cases are built, not sampled.

A problem that looks different

A courier firm packs parcels of sizes between 0 and 1 into vans of capacity 1, and wants to use as few vans as possible. Nobody can compute the minimum for a day's thousands of parcels. What number is surely below the minimum, and what simple packing rule could you charge to it? It is not solved here, and it is not the lab's last problem.

Practise

In the lab you schedule jobs greedily while the drawing shows why the last job can't start too late; predict and then watch the cover that an edge scan builds on a new graph, next to the LP's rounding; write tree doubling and test it against brute force; build instances on which LPT meets its bound exactly and measure how far random instances stay from it; and solve a placement problem that doesn't say what it is.

Recap

You can now: say what an α-approximation guarantees; prove greedy list scheduling (2−1/m)-approximate and build its tight instance; prove LPT's 4/3 core bound and see Graham's tight 4/3−1/(3m) refinement (stated); round the vertex-cover LP and prove the factor 2; prove the maximal-matching cover and tree doubling 2-approximate, naming the lower bound each one uses.

Invariant: every proof charges the algorithm to a lower bound: ALG≤αL and L≤OPT, with L the largest job and the average load, the LP value, the size of a matching, or the weight of a minimum spanning tree.

Complexity achieved: polynomial time (at most O(n2) here, plus solving one LP) within factor 2−1/m, 4/3−1/(3m) (the 4/3 core proved) or 2 of the optimum on every instance, against mn, 2n and (n−1)!/2 for exact search.

Failure mode: trusting a ratio measured on random instances. Worst cases are constructed, and the tight families meet the bound exactly.

In real software: NetworkX's approximation package has min_weighted_vertex_cover, a 2-approximation for weighted vertex cover by the local-ratio method, and christofides for metric tours; see the module notes for versions.

Retrieval (Polytopes and Integrality): on a bipartite graph, does rounding the vertex-cover LP ever lose anything? What does total unimodularity say about the LP's corners there?

Check yourself

  1. Why is greedy list scheduling within 2−1/m of optimal, and where does the −1/m come from?
  2. Round the vertex-cover LP at 0.6, or drop the triangle inequality from the tour problem. What breaks in each proof?
  3. On H, compare LP rounding, the matching cover and the optimum. Which lower bound does each proof charge to, and which algorithm would you use with weights?

Topic list modeled on the public schedule of CMU 15-451/651 Algorithm Design and Analysis (Fall 2025). All lecture text, proofs, examples, code, and exercises are original to SciMigo; no CMU course materials are reproduced. This course is independent and not affiliated with or endorsed by Carnegie Mellon University.

Your turn

Now try it yourself

Write the code, press Run, and scrub through the frames your program draws. The checks tell you when it works.

Loading the lab…

In this lab

  1. Greedy list scheduling, and the job that decides the makespan

    Eight jobs of 4, 7, 3, 6, 5, 2, 8 and 5 minutes arrive, in that order, at three identical machines. Schedule them the greedy way, and make the picture show the step of the proof that the lesson called the one not to skip.

    First, lower_bound(jobs, m): the larger of the biggest job and the total divided by m, rounded up. No schedule can finish earlier, because some machine runs the biggest job and some machine carries at least the average.

    Second, greedy_schedule(jobs, m): take the jobs in order and put each on a machine whose load is smallest so far, the lowest-numbered one on a tie. Return the list machine, where machine[j] is the machine of job j, and call the given show(jobs, machine, m) after every placement: it draws one row per machine and the newest job in amber.

    Third, critical_job(jobs, machine, m): find a machine whose load equals the makespan, the last job j placed on it, and the load s at which j started. Return (j, s). The program then draws j in red and a purple line at the average load of all the other jobs, (total − p_j)/m.

    Before you press Run, predict on paper: which machine gets the 8-minute job, and at what time does it start? Is its start to the left of the purple line? The checks test lower_bound and the greedy rule on random job lists, read every frame to confirm that each job went to a least-loaded machine, and test critical_job against the definition.

  2. Trace two covers: round the LP, then scan the edges

    The lesson covered one graph two ways. Here is a new one, G: a ring of five vertices 0, 1, 2, 3, 4, a vertex 7 joined to 1, 4 and 5, and the edge 5–6. The weights of vertices 0 to 7 are 4, 9, 8, 3, 1, 9, 3 and 3.

    First, rounding. The given lp_half returns the LP optimum x as Fractions. Write rounded(x), the set of vertices with x at least 1/2. The program draws the kept vertices in green and prints their weight next to the LP value.

    Second, the edge scan, which ignores weights. Write scan_cover(n, edges): scan the edges in the listed order, and whenever an edge has neither end taken, take both ends and record the edge as picked. Return (taken, picked), and call the given show(edges, taken, picked, k) after scanning edge k: it draws the taken vertices green, the picked edges blue and the edge being scanned purple.

    Answer the three questions before you press Run, then scrub through the frames: every edge scanned so far should have a green end, and the blue edges should never touch. The checks read every frame, and compare your scan with the rule on 300 random graphs.

  3. Double a tree: a tour within twice the best

    Eight delivery stops sit on a street grid, and the distance between two stops is the Manhattan distance, blocks east–west plus blocks north–south. It is a metric, so the lesson's guarantee applies: tree doubling returns a closed tour at most twice as long as the shortest one.

    Write mst(points), a minimum spanning tree of the complete graph on the points, as a list of n − 1 edges (a, b). Prim's algorithm with an array is the natural choice here: keep, for every point not yet in the tree, its distance to the nearest tree point and which point that is; add the closest one and update the others. That is O(n²) distance evaluations, and the complete graph has n(n − 1)/2 edges anyway.

    Then write tour_by_doubling(points), returning (tree, tour): the tree, and the points in the order a depth-first walk of it from point 0 first reaches them, each exactly once. Any order of children is fine.

    The starter's "tree" is the path that nearest neighbour follows (from each stop, go to the closest unvisited one). A path is a spanning tree too, and on these eight stops its tour is even shorter than the one you will build. But its tree is not minimum, so the proof says nothing about it, and nearest neighbour has no constant-factor guarantee on metric instances. The checks test the tree's weight against their own, check that your tour is a preorder of your tree, and compare its length with a brute-force optimum on 150 small random sets and with your tree on 300 points.

  4. Measure the worst case: random lists against a built trap

    The lesson proved that LPT (largest job first, then greedy) is within 4/3 of the optimum on every list, stated Graham's tight refinement 4/3 − 1/(3m), and said that random tests rarely find the worst case. Measure the stated bound and the claim about random tests.

    Write profile(seed, m). With rng = random.Random(seed), make 100 random lists exactly as its docstring says, and return the worst ratio of greedy to the optimum and of LPT to the optimum, as exact Fractions. The given greedy_makespan, lpt_makespan and best_makespan do the scheduling; best_makespan is exact, by branch and bound, and fine for lists this short.

    Then write lpt_trap(m): a list of jobs on which LPT is exactly 4/3 − 1/(3m) times the optimum, for every m from 2 to 7. You can't run an exact search for every m, so return a schedule too: (jobs, machine), where machine[j] is the machine of job j. If LPT's makespan divided by your schedule's makespan equals the bound, your schedule must be optimal and LPT must meet its bound: the theorem allows no more. The lesson shows the case m = 3; the question is how it grows.

    The program draws, for m = 2 to 5, the worst random ratios of greedy (blue) and LPT (amber), your trap's ratio (red), and LPT's bound (purple). Answer the two questions first. The checks run profile on seeds they choose and compare it with their own count, then check your traps for m = 2 to 7.

  5. Mastery challenge: ambulance stations for Pine County

    Pine County is placing ambulance stations. There are up to 2,000 villages and money for k stations, with k up to 40, each station in a village. The state's response standard looks only at the worst-off village: a plan's worst drive W is the longest drive from any village to its nearest station, and smaller is better.

    Drive times come from the county's service travel(a, b), in minutes. It is symmetric, it is 0 only from a village to itself, and a drive never gets shorter by going through a third village, because the service already reports the fastest route.

    Nobody can compute the best plan for 2,000 villages, so the state accepts a plan whose worst drive is at most twice the best possible, provided it comes with evidence: a list of k + 1 different villages, every two of them at least W minutes apart. Write place_stations(n, k, travel) returning (stations, evidence): at most k different villages (k is at least 1), and the evidence for your plan's W. If every village gets a station, W is 0 and the evidence is [].

    The starter is correct about the plan but tries every set of stations, and it brings no evidence. Every call to travel counts: a check runs two counties of 2,000 villages and 40 stations and allows 3 look-ups per village per station, 240,000 in all. (The count is feedback, not security.)

    Answer the four questions, then write it. The demo draws Pine County's 14 villages with 3 stations: stations in green, the rest of your evidence ringed in red, and what the evidence proves.