Interactive course · ~5 hours

Algorithm Design

Some problems fight back. A fixed rule can have a carefully arranged worst case, and an exact search can grow beyond any practical budget. This course develops several responses: randomise the exposed choice, prove a useful approximation, model the task as flow or linear programming, prune an exact search, isolate a small parameter, and return evidence that lets someone else check the answer.

How this works

Nothing here is quoted from a table. Every count, every average, every rate of being wrong is produced by code running in front of you while you watch, on the data you chose. When a widget says a method was wrong 3.1 percent of the time, it ran the trials and counted.

What you need first

The course Introduction to Algorithms comes before this one. From it you need: counting steps rather than timing them, what O(n) and O(n²) mean as shapes, sorting, and walking a graph. Each idea is taught in one step there, so you can fetch just the piece you are missing. Counting operations instead of reading a clock is Step 1 there. The halving count written log n is Step 3. The pivot and the partition inside quicksort are Step 6. Breadth-first search, which finds every place reachable from a starting point and gets to each by the fewest hops, is Step 9. O(n) and O(n²) as names for growth shapes are Step 13. Everything else is introduced here, including every word that might be new. No maths beyond arithmetic, fractions and percentages is assumed.

The steps

Step 1

Find the median without sorting the list

Here is a task that sounds easy. You have a list of numbers and you want the median: the value that would sit in the middle if the list were sorted. Not the biggest, not the smallest, the middle one. You could sort the whole list and look at the middle. That works, and it does far more work than the question needs, because you asked for one number and sorting delivers all of them in order. Work here means a count of operations, never seconds on a clock. The unit in this step is one comparison of two values. A count is used rather than a stopwatch because a count comes out the same on every machine.

There is a faster way, and it is the sorting method you already met, quicksort, stopped halfway. Pick one value from the list and call it the pivot. Walk the list once and throw every value into one of two piles: smaller than the pivot on the left, bigger on the right. That single pass is called a partition. Now count the left pile. If the middle position you want falls inside the left pile, the right pile can be thrown away entirely. You repeat on what is left.

What is quicksort? I have never met it

Quicksort puts a whole list in order using that partition step and nothing else. Pick a pivot and partition the list around it. The pivot is now sitting in its final place, with everything smaller to its left and everything bigger to its right, so it never has to move again. Then do the same to the left pile and to the right pile, and keep going until the piles hold one value each.

The method in this step is quicksort with one change. It partitions, works out which of the two piles holds the position it is hunting for, and follows only that pile. The other pile is thrown away unsorted, which is why the answer arrives so much sooner than a full sort.

What a list is, and what "position 4" means

A list, also called an array, is a row of values kept in order, one after another, so that each has a position. The positions are counted from 0 in almost all programming, so the first value sits at position 0, the second at position 1, and so on. That position number is called an index. Asking for "the value at index 4" means the fifth one along.

Counting from 0 looks like a mistake for about a week and then stops bothering you. It exists because an index is really a distance: index 4 means "four steps past the start", and the first value is zero steps past the start.

What "the k-th smallest" means, and why the median is a case of it

Line the values up smallest to largest. The 1st smallest is the minimum. The 2nd smallest is the next one along. The k-th smallest is whatever lands in position k of that line-up. The median is the k-th smallest where k is halfway, so in a list of 99 values the median is the 50th smallest.

The method in this step answers the general question,"give me the k-th smallest", and the median is just one choice of k. It has a name: quickselect, because it is quicksort that only follows one of the two halves.

Everything now depends on which value you choose as the pivot. Choose well and each round throws away about half the list, so the total work adds up to roughly one and a bit passes over the data. Choose badly, so the pivot is the smallest value every time, and each round throws away exactly one value, which means n rounds over a shrinking list, which is the shape of a sort again. Here and throughout, n means how many values there are.

Lab 1 · The adversary arranges the list
Try this firstLeave the rule on "always the first value" and press "Let the adversary choose". It builds the list that is worst for that rule and runs quickselect on it, counting comparisons. Then switch the rule to "pick at random" and press it again.
The adversary is not cheating. It is allowed to see your rule and then arrange the input. Against "always the first value" it wins every time, because it always knows which value you are about to pick. Against "pick at random" it has nothing to aim at: it does not know your coin flips, and neither do you until you make them.
Where does a computer get randomness from

A computer does not really flip coins. It runs a small piece of arithmetic that turns one number into another in a scrambled-looking way, then uses that as the next "random" number, then scrambles again. The numbers that come out pass every everyday test for being unpredictable, and the whole sequence is decided by one starting number called the seed. This is called a pseudorandom generator, where pseudo just means "acting like".

That has a useful side effect used all over this course: give it the same seed and you get the same sequence, so a run can be repeated exactly. The widgets here take a seed so that "run it again and see" means something, and so that a surprising result can be shown to someone else.

Why the adversary cannot beat a coin flip

Think of it as a game with two moves. With a fixed rule, you move first by publishing the rule, and the adversary moves second knowing everything. Second player wins.

With a random pivot, the adversary still moves second on the arrangement, but the arrangement no longer decides anything, because the pivot is chosen after the input is fixed and by a process the arrangement cannot touch. Every arrangement becomes equally fine. That is the whole trick, and it is worth saying plainly: the randomness is not there to make the method faster on nice inputs, it is there to remove the idea of a nasty input.

So randomised quickselect has no bad input. It still has bad luck: it is perfectly possible to flip badly ten times in a row. What we can say is what happens on average, and the average is a real measurable thing, not a hope. Run the same list many times with different coin flips, keep a running average of the comparisons used, and watch where it settles.

What an average is here, and why a running one is worth watching

An average of some numbers is their total divided by how many there are. Five runs costing 10, 12, 9, 14 and 15 comparisons average to 60 divided by 5, which is 12.

A running average is that same sum kept up to date as each new run finishes, so after run 1 it is that run, after run 2 it is the mean of two, and so on. Early on it jumps about, because one unlucky run moves it a lot. Later each new run is one voice among hundreds and the line goes flat. Watching it go flat is watching an average stop being a guess.

Lab 2 · The running average settles
Try this firstPress "Run 200 more". The jagged line is each single run's comparison count; the smooth line climbing out of it is the running average. Press it three or four times and watch the smooth line stop moving while the jagged one carries on jumping.
Read the two numbers under the plot. The worst single run is often two or three times the average, and it still happened. That is the deal randomisation offers: no input can make you slow, but luck occasionally can, and the more runs you do the less the total is affected by any one of them.
A rival tells you their quickselect always picks the middle position of the current range as its pivot, and that this is better than random because the middle is a sensible choice. What is the strongest thing you can say against it?
The rule is fixed and public. Taking the middle position is fine on ordinary data and hopeless against data built for it: someone can arrange the list so that the value sitting in the middle position is the smallest one left, round after round. The middle position is not the middle value. Random pivots have no such arrangement, because there is nothing fixed to arrange against.
Step 2

Check two huge files match, and choose your own odds

Two files are supposed to be identical copies. Checking properly means reading both from end to end. If they are a billion characters long, that is a billion comparisons, and you may want an answer sooner than that.

So do something that looks lazy. Pick one position at random and compare just that one character. If they differ there, you have proof: the files are different, and you are finished. If they match there, you have learned almost nothing, so pick another random position and try again.

What a probability is, and how to read"1 in 1000"

A probability is a way of saying how often something happens out of all the times it could. It is written either as a fraction between 0 and 1, or as a percentage, or as"1 in something". A probability of 0.25 is the same as 25 percent and the same as 1 in 4: do it four times and expect it about once.

Small probabilities are easiest to read in the"1 in something" form. A miss chance of 0.001 is 1 in 1000. If a check with that miss chance runs every second, it slips up about once every seventeen minutes. If the miss chance is 1 in a billion, it slips up about once every thirty years. The number only means something once you say how often you are going to ask.

Multiplying a chance by itself, and what a power is

If two things are unrelated, the chance of both happening is the two chances multiplied. One coin landing heads is 1 in 2. Two coins both heads is 1/2 × 1/2, which is 1 in 4. Ten coins all heads is 1/2 multiplied by itself ten times, which is 1 in 1024.

Writing"1/2 multiplied by itself ten times" gets tiring, so it is written 0.510, said as "nought point five to the power ten". The small raised number counts how many copies are being multiplied together, and that is all a power is. The important behaviour: each extra copy halves the result, so ten more probes do not make the miss chance a bit smaller, they make it about a thousand times smaller.

This test has a lopsided kind of honesty, and the shape of it turns up again and again. When it says "different", it is never wrong, because it is holding two characters that disagree. When it says "same", it might be wrong. Errors only go one way, which is called one-sided error. It is what makes such a cheap test usable: you know exactly which of the two answers you are allowed to doubt.

Lab 3 · Probe, and count how often it slips
Try this firstPress "Run 2000 trials". Each trial builds a pair of files that differ somewhere, probes the number of random positions you chose, and records whether it noticed. Then drag the probes slider from 1 up to 12 and press it again at each step.
The two percentages should track each other. One is measured by running trials and counting, the other is worked out from the formula for a miss. They will never be exactly equal, because 2000 trials is a sample rather than the truth, and they should stay close. Now set the difference down to 1 percent of positions and watch how many probes it takes.
Why measured and predicted do not agree exactly

The formula gives the long-run truth. The measurement is 2000 actual trials, and 2000 tosses of a fair coin do not give exactly 1000 heads either. The gap between them shrinks as trials go up, in the same way the running average in Step 1 went flat.

This is worth keeping straight, because it is easy to see a small gap and conclude the formula is wrong. Raise the trial count and watch the gap close. If it does not close, then the formula is wrong.

Now the point of the whole step. You do not have to accept whatever error rate the test happens to give. You choose it. Each extra probe multiplies the miss chance by the same factor, so the error falls off a cliff while the work only creeps up one probe at a time.

Lab 4 · Buying certainty one probe at a time
Try this firstPress "Measure the ladder". It runs real trials at 1, 2, 4, 8, 16 and 32 probes and fills the table with what it measured. Read down the "measured miss rate" column and watch it fall off the bottom.
Look at the last rows carefully. Past a certain point the measured column reads zero, and that does not mean the miss chance is zero. It means it is now smaller than one in the number of trials we ran, so this experiment cannot see it any more. The predicted column keeps going, and that is what a formula is for.
A checker probes 40 random positions. Its designer says the chance of missing a difference is less than one in a trillion, so the check is "as good as certain". Which reply is fairest?
Compare it with the other ways the answer can be wrong. Memory chips flip bits by accident far more often than one in a trillion per use, and hard drives return wrong data occasionally too. Once the chance of the method being wrong is well below the chance of the hardware being wrong, driving it lower buys nothing real. Repeating the check does help, and it never reaches certainty: it just adds more probes, which is what the slider already did.
Step 3

Find a word in a page without re-reading it

Find every place the word the appears in a page of text. The obvious method lines the pattern up at position 0, compares character by character, then slides along one and compares again. For a short pattern that is fine. For a pattern of 500 characters against a document of a million, it is 500 million comparisons, most of them re-reading characters that were compared a moment ago.

Here is another idea. Turn the pattern into one number. Turn each window of the text into one number the same way. Now comparing a window with the pattern is comparing two numbers, which is one operation instead of 500. A short number that stands in for a longer piece of data is called a hash, and when it is used as an identity check like this it is often called a fingerprint.

How a character becomes a number, and what a byte is

Computers store everything as numbers, so every letter has a number agreed in advance. In the oldest and most common agreement, capital A is 65, lower-case a is 97, and a space is 32. Nothing about 65 is special: someone chose it, wrote it down, and everyone followed.

A byte is a number in the range 0 to 255, and it is the standard sized lump that computers move data around in. One byte is exactly enough for one letter in that old agreement, which is why text sizes and file sizes are quoted in bytes. A thousand bytes of plain English is about a thousand letters, or a short paragraph.

What the remainder is, and why every hash takes one

Divide 17 by 5 and you get 3 with 2 left over. That leftover, 2, is the remainder. In code it is written 17 % 5, and the % symbol is read "modulo" or just "mod". The remainder is always smaller than what you divided by, which is the whole reason it is used.

A fingerprint built by multiplying character codes together grows to an enormous number within a dozen characters, far too big to hold or compare quickly. Taking the remainder after dividing by some chosen number, say 1000003, keeps it inside a fixed range for ever, at the price of squashing many different texts onto the same number. That chosen number is called the modulus, and a bigger one means more room and fewer texts squashed together. Handling that price is the last part of this step.

Building each window's number from scratch would cost 500 operations per window, which saves nothing. The move that makes this work is that consecutive windows overlap almost completely. Slide one step and one character leaves the front, one arrives at the back, and everything between is unchanged. So the new number can be worked out from the old one with a subtraction, a multiplication and an addition, no matter how long the pattern is. A hash that can be updated like that is called a rolling hash.

Lab 5 · Roll the window along
Try this firstPress "Step" five or six times. The highlighted window slides one character right each press, and the panel underneath shows the old fingerprint being turned into the new one: character leaving, character arriving, remainder taken. Then press Run to the end and count the matches it found.
Watch the "checked in full" counter. Every time two fingerprints agree, the widget compares the actual characters before calling it a match, and it tells you when the fingerprints agreed but the text did not. Press Use a tiny modulus to make that happen a lot, which is the failure mode this method has to live with.
Two different texts, one fingerprint: collisions

There are far more possible texts than possible fingerprints, so some texts must share. Two different pieces of data with the same hash is called a collision, and no cleverness removes them; the counting argument alone settles it. All you get to choose is how rare they are, by choosing how many different fingerprints exist.

Which is why the match is confirmed character by character before being reported. The fingerprint is a filter that throws out almost every window cheaply, and the expensive full comparison runs only on the few survivors. That two-stage shape, cheap filter then exact confirmation, is one of the most reused ideas in the subject, and Step 4 builds a whole structure out of it.

Why multiply by a base at all

The fingerprint here is built the way you build a number out of digits. In an ordinary number, 472 means 4×100 + 7×10 + 2. The rolling hash does the same with character codes instead of digits and a different base instead of ten, then takes a remainder. Choosing a base bigger than the alphabet is what makes position matter, so that abc and cba get different fingerprints.

Add the codes instead of building a number and every anagram collides, which for text search would be a disaster. You can see this by choosing the just add the codes option in the lab above.

Lab 6 · Write the roll yourself
Try this firstPress "Check against hidden texts" straight away. It fails, and the message names the first window where your answer disagreed with a fingerprint built from scratch. Then fill in the body of roll and check again.
The checker never reads your text. It calls your function on windows of texts you cannot see, including a one-character pattern and a window that has just wrapped past a space, and compares each answer against the fingerprint computed the slow honest way.
A search using a rolling hash reports 6 matches, and the text really does contain the pattern 6 times. During the run, fingerprints agreed 9 times. What happened?
Three collisions, caught by the confirmation step. Agreeing fingerprints is a reason to look, not a result. Since the reported count is right, the full comparison did its job. A larger modulus would make collisions rarer, not more common, and the number of real matches never depends on the modulus at all: that is the property that makes the method safe to tune.
Step 4

Remember 300 million addresses in a few megabytes

A web crawler visits pages and must not visit the same one twice. It has seen 300 million addresses so far. Keeping them all so it can ask "seen this one?" costs many gigabytes, and the question it actually needs answered is only ever yes or no.

So keep no addresses at all. Keep a long row of bits, every one starting at 0. To record an address, run it through three different hash functions, which gives three positions in the row, and set those three bits to 1. To ask about an address later, hash it the same three ways and look: if any of the three bits is 0, that address was definitely never added, because adding it would have set that bit.

What a bit is, and how small this really is

A bit is the smallest piece of information there is: one thing that is either 0 or 1. A byte, from Step 3, is eight bits bundled together, which is why a byte holds 256 different values, since eight yes-or-no answers can be combined in 256 ways.

Now the sizes. A web address is perhaps 60 characters, so 60 bytes, which is 480 bits. The structure in this step typically spends about 10 bits per address, whatever the addresses look like. That is roughly one fiftieth of the space, and it is why this structure gets used at all.

What a hash function is doing here, and why three of them

In Step 3 a hash turned text into a number that stood in for it. Here it turns text into a position in the row of bits, by taking the remainder after dividing by the row length. A good hash spreads unrelated inputs evenly across the row, so two different addresses rarely land in the same place.

Rarely is not never, so one bit is a weak claim. Three separate hash functions, each landing somewhere different, make the claim three times over. For a stranger to be mistaken for a member, all three of its bits must already have been set by other members, which is much less likely than one being set. The lab below lets you set the number of hash functions and see the effect.

Notice the shape of what you can conclude. When the structure says "never seen it", it is telling the truth, always. When it says "possibly seen it", it might be wrong, and that mistake is called a false positive. This is the one-sided error from Step 2 in a different costume, and once again the useful part is knowing exactly which answer to doubt. The structure is called a Bloom filter, after Burton Bloom, who described it in 1970.

Lab 7 · Build one and make it lie to you
Try this firstPress "Add" on three or four of the words in the top row and watch bits light up in the grid. Then press the words in the bottom row, which were never added, and look for one that comes back "possibly seen". Press Add ten more until you find one.
Nothing was stored. Scroll the grid: it is only 1s and 0s, and no word can be read back out of it. A Bloom filter cannot list what it contains, cannot remove anything, and cannot tell you which word set a given bit. It answers exactly one question, which is what makes it so small.
Why more hash functions eventually make things worse

Each extra hash function adds another bit that a stranger has to be unlucky in, which pushes the false positive rate down. It also sets another bit per member, which fills the row faster, which pushes the rate up. Those two effects pull in opposite directions, so there is a best number of hash functions for a given row length and a given number of members, and going past it is worse than stopping short.

The best value works out to about 0.7 times the row length divided by the number of members. You do not have to take that on trust: the next lab measures the rate for every setting, so you can find the low point by looking.

Lab 8 · Tune it, and measure what you get
Try this firstPress "Measure every k from 1 to 10". For each number of hash functions it fills a fresh filter with the same members, then tests 20000 strangers and counts how many were wrongly accepted. The lowest bar is the best setting for the row length you chose.
Move the "bits per member" slider and measure again. At 4 bits per member nothing you do gets the rate below a few percent. At 16 bits per member the best setting is under a tenth of a percent. Space and accuracy trade against each other directly, and this widget is the exchange rate.
A spell checker keeps its dictionary in a Bloom filter to save space. What can go wrong for a user?
A misspelling slips through. The filter never says "not in the dictionary" about a word that is in it, so a real word is never underlined. The error runs the other way: a nonsense word can hash onto bits that other words already set, and be waved past. For a spell checker that is a fine trade, and for a bank checking whether an account exists it would not be, which is how you decide whether to use one.
Step 5

Work out the real cost of one push onto a growing list

A list in most languages can grow. You add an item on the end and it just fits, for ever, however many you add. Underneath, memory does not work like that. The list owns a block of a fixed size, and when that block is full there is nowhere to put the next item.

What actually happens is: ask for a bigger block, copy every existing item into it, throw the old block away, then put the new item in. So most additions cost one small step, and occasionally one costs as much as the entire list length. A cost that grows in step with the number of items like that is written O(n) and said "order n". Adding an item on the end is called a push, and the size of the block is called the capacity, which is not the same as the number of items in it.

Why a block of memory cannot simply be extended

Memory is one enormous row of numbered slots, and a list lives in a run of neighbouring slots so that reaching item 5000 is one piece of arithmetic rather than a search. Neighbouring is the important word. The slot just past the end of your block is not spare; it very likely belongs to something else already.

So growing means finding a free run that is long enough somewhere else, and moving. The copy is not clumsiness that a better language would avoid. It is what the shape of memory costs.

What "cost" means when nobody is timing anything

Throughout this course, cost is a count of operations, not a stopwatch reading. In this step one unit of cost is one item copied, or one item written into a slot. Counting is used rather than timing because a count is the same on every machine, on a busy laptop and a quiet one, in ten years' time. This is the habit from Introduction to Algorithms, and it is why the widgets show counts.

A stopwatch is not useless, it just answers a different question: how fast is this machine today. A count answers how much work the method demands, which is the part you get to design.

What is O(n), and what does "quadratic" describe?

O(n) names the shape of a count rather than the count itself, and is said "oh of n" or "order n". Describing costs with that letter O is called big-O notation, which is the name you will see elsewhere. It means the count grows in step with n: double the number of items and the count doubles. It throws fixed multipliers away on purpose, so a method doing 3n steps and one doing 100n steps are both O(n). That makes it useful for picking a shape and useless for choosing between two methods of the same shape.

O(n²) means n multiplied by itself, so doubling the number of items makes the count four times as large. That shape has a name of its own, quadratic, and it is the difference between a method that copes with a thousand items and one that cannot cope with a million.

Now the question that matters. If one push in a thousand costs a thousand, is pushing cheap or expensive? Add up the whole run and divide by the number of pushes. That number, the total cost of a sequence divided by the length of the sequence, is called the amortised cost, and amortised is an accountant's word for spreading a lump sum over the period it covers.

Lab 9 · Push, and watch the copies
Try this firstPress "Push 1" about ten times, slowly. Most presses add a bar of height 1 to the cost plot. Every so often a tall spike appears: that is a resize copying everything. Then press Push 200 and look at the average line, which stays flat while the spikes get taller.
The spikes get taller and rarer at exactly the same rate. Doubling means the next copy is twice as expensive as the last one and happens after twice as many cheap pushes, so each doubling contributes the same amount of copying per push. That is why the average line is flat rather than slowly rising.
Amortised is not the same as average from Step 1

In Step 1 the average was over luck: run the same input many times with different coin flips and the cost varies. Amortised cost has no luck in it at all. It is a promise about one single run: any sequence of n pushes costs at most a fixed amount times n, every time, guaranteed.

The two get confused because both end in a division. The difference matters when something goes wrong: a bad random run is unlucky and the next one will probably be fine, while an amortised bound that fails is simply false.

There is a way of proving that flatness that needs no algebra, and it is where the step's name comes from. Charge each push more than it costs. Spend part of the charge on the actual work, and put the rest in a bank. When the expensive copy arrives, pay for it out of the bank. If the bank balance never goes negative, the charge you invented is an honest upper bound on the real average cost, because you never spent money you did not collect.

Lab 10 · Find the charge that keeps the bank solvent
Try this firstSet the charge to 1 and press "Run 64 pushes". The bank goes negative almost at once and the widget says which push broke it. Raise the charge to 2, then 3, and find the smallest whole charge that survives all 64 pushes.
Then switch the growth rule to "add 10 slots". No fixed charge works any more, however high you set it, because the copies grow while the gaps between them stay the same length. Growing by a fixed number of slots is the version that looks sensible and is quadratic.
Your list has 1024 items and capacity 1024, so the next push triggers a copy of 1024 items. A colleague says pushing is therefore O(n) and the amortised claim is marketing. Where is the hole in that?
Both statements are true and they answer different questions. The worst single push is O(n), and no bookkeeping hides that. The amortised claim is about totals: those 1024 units of copying arrive only after 512 pushes that cost one unit each, and 1024 spread over that run is a small constant per push. If you are writing something where one slow push would be a disaster, such as software controlling a machine, the worst case is the number you care about, and there are list designs that spread the copy out to keep it low.
Step 6

Cover every corridor with cameras, within twice the best

A museum has corridors, and cameras can be placed at the junctions. A camera at a junction watches every corridor that touches it. You want every corridor watched, using as few cameras as possible. Draw the junctions as dots and the corridors as lines between them, and this is a graph: the dots are called nodes and the lines are called edges. A set of nodes that touches every edge is called a vertex cover, vertex being another word for node.

Nobody knows a method that finds the smallest cover quickly on every graph. Not "nobody has bothered": this problem is one of a large family, all provably equivalent to each other, and a fast method for one would give a fast method for thousands of famous problems at once. Several decades of serious effort have produced neither a method nor a proof that none exists.

Graph words, in one place

A graph is dots and lines: nodes joined by edges. It is a shape for relationships, not a picture of anything physical, so the same graph can be drawn a hundred ways and still be the same graph. Junctions and corridors, people and friendships, cities and roads, web pages and links are all graphs.

The number of edges touching a node is its degree. An edge is covered by a set of nodes if at least one of its two ends is in the set. Introduction to Algorithms used graphs for searching and shortest paths; here the graph is the problem itself rather than a map to walk.

Why not just try every possibility

You can, and for a graph of 20 nodes it is instant. Each node is either in the cover or out, which is 2 choices per node, so 2 multiplied by itself 20 times, which is about a million sets to test. Fine.

At 60 nodes it is 2 multiplied by itself 60 times, which is about a billion billion sets. At a million tests per second that is thirty billion years. The graphs in the lab below are deliberately small enough that the exact answer can be found by trying everything, which is what lets the widget show you the true optimum to compare against.

So change the question. Instead of the smallest cover, ask for a cover that is never worse than twice the smallest, found quickly, on every graph, guaranteed. That factor is called the approximation ratio, and a method carrying one is worth far more than a method that is usually good: usually good has no worst case you can quote to anybody.

Lab 11 · Cover it yourself, then see the optimum
Try this firstClick nodes in the picture to switch cameras on and off. Uncovered corridors stay grey and covered ones turn blue, and the counter says how many are still uncovered. Cover them all with as few cameras as you can, then press Reveal the true smallest.
Now run both automatic methods on the trap graph. Greedy takes whichever node covers the most uncovered corridors, and on that graph it beats the both-ends method and still misses the smallest cover. The both-ends method lands at exactly twice the smallest, the worst it is ever allowed to do. So the one that wins here is the one with no promise at all, and on a larger graph nothing stops greedy being many times too big.
Why taking both ends of an edge, which looks wasteful, is the whole proof

The method with the guarantee is almost embarrassing. Find any edge that is not yet covered, take both of its ends into your cover, cross out every edge those two nodes touch, and repeat. Half of every pair you take may well be pointless.

Look at the edges you picked. No two of them share a node, because after picking one you crossed out everything touching it. So any cover at all, including the smallest possible one, must contain at least one end of each of those edges, and those ends are all different nodes, so the smallest cover has at least as many nodes as you picked edges. You took exactly two nodes per picked edge. Twice at most, and the argument never once mentions what the graph looks like.

Lab 12 · Write a cover that keeps the promise
Try this firstPress "Check on hidden graphs" before changing anything. The starting code returns an empty cover, and the message names an edge it left uncovered. Then write the loop that takes both ends of each uncovered edge.
The checker tests two things separately. First, every edge is covered, on graphs including one with no edges at all. Second, your cover is at most twice the true smallest, which it works out by trying every subset on graphs small enough for that. A cover that takes every node passes the first test and fails the second.
On some graph, the both-ends method returns a cover of 14 nodes. Without seeing the graph, what do you know about the smallest possible cover?
At least 7. The 14 came from picking 7 edges that share no nodes, and every cover must include an end of each of those 7, so no cover is smaller than 7. It could be 7, 9, 13 or 14. This is the quietly useful half of an approximation guarantee: it gives you a lower bound on the perfect answer for free, so you can sometimes tell that what you have is already optimal.
Step 7

Get a delivery round within twice the shortest

A delivery van must visit twenty stops and come back to the depot. Which order makes the shortest round trip? This is the travelling salesman problem, and it belongs to the same family as Step 6: no fast exact method is known for it either.

Trying every order is worse than trying every subset. With 20 stops there are 19 × 18 × 17 and so on down to 1 different tours, which is about 120 million billion. Multiplying every whole number from 1 up to some n like that is written n! and called a factorial, and it grows faster than any power.

How far apart are two stops on a map

Give every stop two numbers, how far right it is and how far up it is, which are called its coordinates. Two stops make a right-angled triangle: the difference in the across numbers is one short side, the difference in the up numbers is the other, and the straight-line distance is the long side.

The rule for the long side is Pythagoras: square each short side, add them, then take the square root. If one stop is 3 across and 4 up from the other, that is 9 + 16 = 25, and the square root of 25 is 5. Every distance in this step and in Step 12 is worked out that way, in code, as you watch.

What a tour is, exactly

A tour is an order for visiting all the stops that starts at one of them, visits every other exactly once, and returns to the start. Its length is the sum of the distances between consecutive stops, including the hop from the last one back to the first.

Because the tour comes back to where it began, where you start makes no difference to the length, and neither does going round it the other way. That is why 20 stops give 19! tours rather than 20!, and it is the first hint that a lot of the apparent difficulty is bookkeeping rather than substance.

So we settle again, but differently from Step 6. Here there are two moves. First build any tour quickly: stand at the depot, go to the nearest stop you have not visited, repeat. That is nearest neighbour, and it produces a decent tour with one dreadful habit, which you can see the moment you run it. Then improve what you have, by looking for two edges that cross and swapping them.

Lab 13 · Beat the machine at a round trip
Try this firstClick the stops one at a time to build your own tour, in whatever order looks short to you. The line follows your clicks and the length updates. When you have visited them all, press Nearest neighbour and compare.
Look at nearest neighbour's last few moves. It takes every easy short hop early and leaves a scattering of stragglers, then has to cross the whole map to collect them. Being greedy about the next step means the leftovers are the ones nobody wanted, which is a habit worth recognising well beyond this problem.
What a local improvement is, and where it stops

A local improvement is a small change you can test cheaply: swap two things, and keep the swap only if the total got smaller. Repeat until no single swap helps any more. The tour you end on is called locally optimal, meaning no small change improves it, which is not at all the same as being the best tour.

Picture standing on a hillside in fog and always walking downhill. You will reach somewhere flat. Whether it is the bottom of the valley or a dip halfway up depends entirely on where you started, which is why the lab lets you rerun from a different starting tour and get a different ending.

Lab 14 · Two-opt pulls the knots out
Try this firstPress "Find one improving swap". It hunts for two edges that can be replaced by two shorter ones, shows the pair it found in orange, and applies it. Press it repeatedly and watch the crossings disappear one at a time, then press Run to the end.
Two crossing edges are always worth swapping. That falls out of the triangle rule: any two sides of a triangle are longer than the third, so uncrossing a pair always shortens the tour. So a locally optimal tour under this move never crosses itself, which is why the finished picture looks tidy. Tidy is not optimal, and the counter tells you how far above the best known tour you finished.
You run two-opt from twenty different random starting tours and keep the best result. Why is this usually better than running it once from the nearest neighbour tour?
Different starts, different dips. Local improvement always ends somewhere flat, and which flat place depends on where it began. Twenty attempts cost twenty times as much and give you the best of twenty, which is usually a real gain. Random tours start much longer than nearest neighbour, so a single random restart is usually worse; it is having many that pays.
Step 8

Push as much water through the pipes as they will carry

A network of pipes runs from a reservoir to a town. Each pipe has a direction and a limit on how many litres a second it can carry, which is its capacity. Water cannot pile up at a junction, so whatever arrives must leave. How much can reach the town per second?

The starting point is the reservoir, called the source. The destination is the town, called the sink. Every other junction obeys the rule that what comes in goes out, which is called conservation. The amount arriving at the sink is the value of the flow, and the largest value any legal arrangement achieves is the maximum flow.

A directed graph, and why direction matters here

The graphs in Step 6 had plain edges: an edge between A and B works both ways. A directed edge points, drawn as an arrow, and it can only be used in the direction it points. A one-way street, a pump that only pushes one way, a job that can be given to a person but not the reverse.

An edge here carries two numbers: how much it is currently carrying, and the most it could carry. They are drawn as 2/5, meaning two litres flowing through a pipe that could take five. When those two numbers become equal the pipe is saturated and nothing more fits.

Why this problem is worth a whole part of the course

Almost nothing that uses maximum flow involves water. Assigning workers to shifts, matching students to schools, choosing which projects to fund, separating an object from the background in a photograph, and working out which roads to cut to divide a city are all maximum flow underneath. Step 10 does one of these conversions in full.

Pipes are the picture that makes the rules obvious. Conservation is just "nothing appears or vanishes at a junction", and everybody already believes that about water. Keep the picture, and remember that the pipes are usually a costume.

How would I find a route from the source to the town in the first place?

By walking outwards from the source. Mark the source. Look at every pipe leaving a marked junction that still has room in it, and mark the junction at its far end. Repeat until a round of looking marks nothing new. Every junction marked this way is reachable from the source. If the sink ends up marked, a route to it exists: note which pipe marked each junction and you can trace it back.

Which marked junction you look at next decides which route you find. Take them in the order they were marked and the search spreads outwards in rings, so it arrives at the sink using the fewest pipes any route could use. That is what "shortest path" means when every pipe counts as one step, and it is the fewest-hops search Introduction to Algorithms called breadth-first.

The method is one idea repeated. Find any route from source to sink whose every pipe has room left. Push as much as the tightest pipe on that route allows, which is that route's bottleneck. Repeat until no such route exists. A route with room left is called an augmenting path, and the whole method is called Ford-Fulkerson after the two people who wrote it down in 1956.

Lab 15 · Ford-Fulkerson, one path at a time
Try this firstPress "Find an augmenting path". A route from S to T lights up in purple and the panel names its bottleneck. Then press Push flow along it and watch the numbers on those pipes climb and the flow value rise. Repeat until the widget tells you no path is left.
Change a capacity and press "Start the flow over". The row of boxes under the picture holds one number per pipe. Raising the capacity of a pipe that was never saturated changes nothing at all, and raising one that was full may or may not help, depending on what is downstream of it. Finding out which pipes actually matter is the whole of Step 9.
The residual edge: why you are allowed to take flow back

Push 3 litres along a pipe with capacity 5 and two things become true. There is room for 2 more in the forward direction, and there are 3 litres already there that a later path is allowed to cancel. That second option is drawn as a faint backward arrow of size 3, and it is called a residual edge. Residual just means what is left over, and here it means what is still possible.

Without it the method gets stuck. A path chosen early can be exactly the wrong commitment, and the backward arrow is how a later path says "send that water somewhere else and I will take over your route". Nothing physically flows backwards; it is bookkeeping that lets the method change its mind without starting again. Press Load the cancellation example in the lab. Four early paths are already filled in. The purple fifth path uses one bright backward arrow to undo part of the third path, which lets three more litres reach T.

Does the order of the paths change the answer

No. Different orders produce different arrangements of water, sometimes very different ones, and the total arriving at the sink at the end is always the same number. Step 9 explains why and the explanation is the best part of this whole subject.

The order does change how much work it takes. The lab offers a shortest-path chooser, which always takes the path with fewest pipes, and that choice puts a firm limit on the number of rounds no matter how large the capacities are. The network as it loads is too small to tell the two apart: both need three rounds. Raise the capacity of A→C from 5 to 8 and run each chooser again and the shortest-path one takes three rounds where the other takes four, for the same 17 litres a second.

Lab 16 · Predict the maximum before you run it
Try this firstLook at the small network, type your prediction into the box, and press "Lock in my answer". Only then does the Run button appear. Five networks, each with a different reason why the obvious guess is wrong.
Adding the capacities leaving the source is almost always too high. The limit is set somewhere in the middle of the network, not at either end, and by the time you have done five of these you will start looking for the narrow place instead of counting the pipes at the edges. That instinct is what Step 9 turns into a theorem.
In a network where every capacity is a whole number, you have pushed 12 litres and just found an augmenting path whose bottleneck is 3. What is true of the maximum flow?
At least 15. Pushing along that path is legal, so 15 is achievable, and the maximum is by definition no smaller than anything achievable. Whether it stops at 15 depends on whether another augmenting path exists afterwards, which you cannot tell without looking. Every round gives a better lower bound and never an upper one, which is exactly the gap Step 9 fills.
Step 9

Prove nothing more can get through, by finding the bottleneck

Step 8 left a gap. Every augmenting path proves the maximum is at least something. Nothing so far ever proves it is at most something, so when the paths run out you have a number and no reason to believe it is the best possible rather than the best you happened to find.

Here is the missing half. Split the junctions into two groups: one containing the source, the other containing the sink. Every junction goes in exactly one group. That split is called a cut. Now add up the capacities of the pipes that run from the source's group to the sink's group. That total is the capacity of the cut, and no arrangement of water can beat it, because every litre reaching the town has to cross from one group to the other somewhere.

A cut, carefully: which pipes are counted

Draw a line separating the two groups. Only pipes pointing from the source side to the sink side are counted. Pipes pointing the other way, from the sink side back to the source side, are not counted, and this is the part everybody gets wrong first.

The reason is that a backward pipe cannot help water get across; it can only carry water back the way it came, which has to cross forwards again somewhere to be useful. So the forward capacities alone are the ceiling. Counting the backward ones too would give a larger, weaker ceiling that is still true and no longer tight.

Why every cut is a ceiling, in one picture

Take any legal flow and any cut. Every litre starts at the source, on the source side, and finishes at the sink, on the other side. It cannot teleport. Somewhere along its route it crosses from one side to the other, and the only way across is a pipe pointing that way.

So the total crossing forwards is at least the flow value, and each pipe can carry no more than its capacity. Hence flow value is at most the cut capacity. This holds for every flow and every cut at once, so the largest possible flow is at most the smallest possible cut. That inequality is free. The surprise, coming next, is that it is never slack.

Lab 17 · Cut it yourself and squeeze the ceiling down
Try this firstClick junctions to move them onto the source side. S is always on the source side and T is never. The counted pipes turn red and the total updates as you go. Find the smallest total you can, then press Compare with the maximum flow.
Every cut you build is a valid ceiling. Put everything except T on the source side and you get one number; put only S there and you get another. Both are true statements about the maximum flow, and the useful one is whichever is smallest. There are 2 to the power of the number of middle junctions cuts to choose from, so hunting by hand does not scale, which is what makes the next lab worth having.

Now the result that ties Part 3 together. When Ford-Fulkerson stops, it has not merely run out of ideas. Take the set of junctions you can still reach from the source using pipes that have room left, counting the backward residual arrows from Step 8 as usable. Reaching means walking outwards: mark every junction a usable pipe leads to, and keep going until nothing new gets marked. Call that set the source side. That is a cut, and its capacity is exactly the flow you achieved.

Lab 18 · The cut appears on its own
Try this firstPress "Run the flow to completion", then press "Mark what is still reachable". The reachable junctions turn blue, the crossing pipes turn red, and the two totals at the bottom, flow value and cut capacity, are computed separately and come out equal.
Check the red pipes. Every pipe crossing forwards is full, and every pipe crossing backwards is empty. That is forced: a forward pipe with room left would have let the search continue, and a backward pipe carrying water would have offered a residual arrow to come back along. So the cut is saturated in both directions, which is why its capacity equals the flow exactly.
What this theorem is worth outside pipes

Two quantities defined in completely different ways, the largest flow and the smallest cut, are always the same number. One is a maximum over arrangements of water, the other a minimum over ways of splitting a set of junctions. Results of that shape are called duality, and they turn up everywhere: Step 11 meets the general version.

Practically, the cut is often the answer people actually wanted. Which links must fail before a network is severed. Which pixels are object and which are background, in a photo editing tool where capacities encode how similar neighbouring pixels are. Which set of teams is over-subscribed in a rota. The flow computes it; the cut explains it.

You compute the maximum flow of a network and get 20, and you also find a cut of capacity 20. Your colleague suggests spending money to widen a pipe that crosses that cut. Is that worth doing?
Widen one, and the cut is still there. If the cut has four pipes crossing it and you widen one by 5, that cut's capacity goes up by 5, so the ceiling rises. But another cut may now be the smallest, and the flow only rises to whatever the new smallest cut allows. This is exactly how the result is used in practice: the min cut names every place worth spending money, and tells you when spending on one of them alone is wasted.
Step 10

Match six people to six jobs with the flow machine

Six people, six jobs. Each person is willing to do some of the jobs and not others. Each person takes one job, each job goes to one person. How many can be given work at the same time?

Draw people as dots on the left, jobs as dots on the right, and an edge wherever a person is willing to do a job. A graph whose nodes fall into two groups, with every edge crossing between the groups and none inside a group, is called bipartite, which means two parts. A set of edges no two of which share a node is a matching: nobody is asked to do two jobs and no job goes to two people.

Why "no two share a node" is the whole definition

Every restriction in the problem comes from that one sentence. Two chosen edges touching the same person would mean that person doing two jobs. Two chosen edges touching the same job would mean two people doing it. Forbid shared nodes and both are handled at once.

A matching that uses every node on one side is called perfect for that side. The interesting question is usually not whether a perfect matching exists but how large the best matching is, since that is the number of people who get work.

Why the obvious greedy method is not enough

Go down the list of people and give each the first free job they are willing to do. Fast, and wrong. If Ana is willing to do only job 1, and Ben is willing to do job 1 or job 2, then handling Ben first may hand him job 1 and leave Ana with nothing, when both could have worked.

Fixing it means being willing to take a job back off someone and move them, which is precisely the backward residual arrow from Step 8. That is not an analogy: once the problem is converted to flow, the residual arrows do exactly this reshuffling for you, and you never write that logic at all.

The conversion is three lines. Add a source with an arrow into every person, capacity 1. Keep every willingness edge, pointing from person to job, capacity 1. Add an arrow from every job to a sink, capacity 1. Run maximum flow. Every whole litre that gets through has used exactly one person and one job, so the flow value is the size of the largest matching, and the pipes carrying water are the pairs.

Lab 19 · Assign the jobs, then let the flow do it
Try this firstClick a person, then click a job they are joined to. That pairs them, and the counter says how many are matched. Get as many as you can, then press Solve it with flow and see whether the machine found more.
Press "Show the flow network" to see the costume. The same picture gains a source on the far left, a sink on the far right, and every edge gets capacity 1. Nothing about the matching problem was changed; it was restated so that a method you already have can answer it.
Why capacity 1 on the source arrows is doing so much work

Capacities are the only place the rules live. A capacity of 1 into a person means at most one litre can ever be inside that person's dot, so at most one willingness edge out of them can carry anything. That single number is the sentence "one job each".

Change it to 2 and you have said each person may take two jobs, and the same solver answers the new question with no other change. That is what makes flow such a widely used tool: a surprising amount of real-world fussiness is expressible as capacities and the solver never needs to know what the capacities mean.

Lab 20 · Why some people cannot be placed
Try this firstPress "Solve and explain". When the matching cannot reach everybody, the widget takes the minimum cut from Step 9 and turns it back into plain words: a group of people who between them are willing to do too few jobs. Press Another example for a fresh one.
This is a proof, not an excuse. If five people between them will accept only three jobs, then at most three of them can work, whatever anyone tries. The min cut finds that group for you, which turns "the computer says four" into "these five people share three jobs, so four is the most there is", which is the version you can take to a person and act on.
A matching problem has 8 people and 8 jobs, and the maximum flow comes out at 6. What does the minimum cut tell you that the number 6 does not?
It names the bottleneck. The cut splits the picture, and reading it back gives a set of people together with the smaller set of jobs they are collectively willing to do. That is the reason 6 is the ceiling, and it survives any reordering, so it is not about luck or solver choices. It also tells you what to change: widen those people's willingness, or add a job they would accept.
Step 11

Work out how many chairs and tables to make

A workshop makes chairs and tables. Each chair takes 2 hours of work and 1 plank; each table takes 3 hours and 4 planks. There are 24 hours and 20 planks in a week. Chairs sell for 30 and tables for 50. How many of each should be made?

Every sentence there is the same shape: some amounts multiplied by fixed numbers, added up, and required to stay under a limit. A requirement of that shape is a constraint, and the thing you are trying to make as large as possible is the objective. A problem made only of those pieces is a linear program, and "program" here means a plan or schedule, a word older than computers.

Reading an inequality, and drawing one

The symbol ≤ means "is less than or equal to". Writing 2c + 3t ≤ 24 says: two hours for each chair plus three for each table, added up, must not exceed 24. It is not an equation to solve, it is a filter that some pairs of numbers pass and others fail.

Draw chairs across and tables up. Every pair of amounts is a point. The pairs that use exactly 24 hours form a straight line, and the pairs that use fewer are everything on one side of it. So one constraint cuts the picture in half, and several constraints together leave a shape with straight sides.

The feasible region, and what a corner is

The shape left after all the constraints have had their say is the feasible region: every point inside it is a plan you could actually carry out. Outside it you have run out of hours, or planks, or you are making a negative number of chairs.

A corner of that shape is a point where two of the constraint lines meet, and where both are exactly used up: all 24 hours and all 20 planks, with nothing spare. Corners are special because there are only a few of them and one of them is always the answer.

The value of a plan is 30 chairs plus 50 tables, and the plans worth exactly 600 also form a straight line. Sliding that line outwards raises the value and the best plan is the last point of the region the line touches on its way out. A straight line leaving a straight-sided shape leaves through a corner, which is why an optimum is always at a corner, and why checking a handful of corners is enough.

Lab 21 · Tilt the objective and watch the answer jump
Try this firstDrag the "price of a table" slider slowly from left to right. The dashed green line tilts, and the winning corner, marked in green, stays put for a while and then jumps to the next corner. Watch the numbers under the picture as it jumps.
Notice what jumping means. A tiny change in the price of a table changes the best plan from one recipe to a completely different one, with no plans in between. The answer to a linear program is not a smooth function of its inputs, which is why businesses run these again after every price change rather than adjusting last week's answer.
Why the best plan is never strictly inside the region

Suppose the best plan sat somewhere in the middle, not touching any constraint. Then you have hours spare and planks spare, so you can make one more chair and be better off. That contradicts it being best.

The same argument pushes you along an edge until you reach a point where you cannot move in any improving direction without breaking something, and that point is a corner. This is the reason the standard method for solving these, the simplex method, walks from corner to corner rather than searching the interior.

Flow was a linear program all along

Give every pipe in Step 8 an amount. The constraints are "each amount is between zero and that pipe's capacity" and "at each junction, in equals out". The objective is the total arriving at the sink. Every one of those is amounts multiplied by fixed numbers and added, so max flow is a linear program with one variable per pipe.

The min cut is its dual, which is the general version of the pairing in Step 9: every maximisation of this shape has a matching minimisation whose answer is the same number. Ford-Fulkerson is faster than a general solver because it exploits the pipes, and it is solving an instance of the same thing.

Lab 22 · Your own workshop
Try this firstChange any of the six numbers and watch the region redraw. Nothing here is marked and nothing is being checked. Try setting both numbers in the chair column to 0 and see what the widget says about the answer running away.
Two failures are worth causing on purpose. Remove all the limits and the objective can grow for ever, which is called unbounded. Add a constraint demanding at least 20 tables while the hours only allow 8, and no plan satisfies everything, which is called infeasible. Real solvers report both by name, and now you know what they are describing.
A solver reports the best plan as 4.5 chairs and 2.8 tables. The workshop cannot make half a chair. What is the safest thing to do?
Use it as a ceiling and search nearby. Rounding up can break a constraint outright: 5 chairs and 3 tables may need more hours than exist. Demanding whole numbers is a real thing to want, and it changes the problem into integer programming, which is in the hard family from Step 6 and needs a different solver. What the fractional answer always gives you for free is a bound no whole-number plan can beat, which is how such solvers prune their search.
Step 12

Find the closest two aircraft without checking every pair

A thousand aircraft on a radar screen. Which two are closest together? Compare every pair and that is 499500 distance calculations, and it has to finish before the picture updates.

Splitting the problem in half is the obvious first move, and it is the move that seems to fail. Sort the points left to right, cut down the middle, and find the closest pair in each half. Now you have two answers and a hole: the closest pair overall might be one point from the left half and one from the right. Nothing you have computed says anything about those.

Divide and conquer, briefly recalled

Solve a big problem by cutting it into smaller copies of itself, solving those the same way, and combining. Mergesort in Introduction to Algorithms did this: sort each half, then merge. A method that calls itself on smaller inputs is recursive, and the smallest case, where it answers directly instead of splitting, is the base case. Here the base case is three points or fewer, which you just compare directly.

Whether divide and conquer helps depends on the combining cost. If splitting creates smaller easy cases but recombining them repeats the original amount of work at every level, the method gains little. This step derives a linear-time combine for the closest-pair problem.

Why the hole in the middle is not as big as it looks

Call the better of the two half answers d. A crossing pair only matters if it is closer than d. Both of its points must therefore be within d of the dividing line, because anything further away is already more than d from everything on the other side. So only points in a narrow strip of width 2d need considering at all, and usually that is a handful.

That alone is not enough, because someone could arrange every point inside the strip. The second idea is what saves it: sort the strip's points bottom to top, and for each one, only compare it with the next few above it. Points further up than d cannot be within d, and the strip is only 2d wide, so no more than a fixed handful can be packed into that region without two of them being closer than d to each other, which cannot happen since d was the best in each half.

What is log n, and what does "a log factor" cost me?

log n counts how many times you can halve n before you are down to 1. Halve 16 and you get 8, then 4, then 2, then 1: four halvings, so log 16 is 4. Doubling n adds exactly one to the count, which is why a thousand points give about ten and a million give about twenty.

It turns up here because a method that cuts its input in half over and over has about log n levels of cutting. A method described as slower "by a log factor" pays some cost once per level instead of once in total. On a million points that is about twenty times the work: a real penalty, and still nothing like the gap between the two methods raced below.

Lab 23 · Every pair, raced against the split
Try this firstPress "Race them" with the slider at 200 points. Both methods run on the same points and both report the same closest pair, and the distance-calculation counters differ by a lot. Then drag the slider up to 2000 and race again.
Watch the ratio column, not the raw counts. Quadrupling the number of points multiplies the brute force count by about 16 and the divide and conquer count by about 4 and a bit. That gap is the difference between a radar that keeps up and one that does not, and it is measured here by counting, exactly as Introduction to Algorithms did.
Lab 24 · Watch the strip do its job
Try this firstPress "Next stage" repeatedly. It walks the recursion: the dividing line appears, each half is solved, then the shaded strip appears and only the pairs inside it are tested. The panel counts how many pairs the strip actually examined.
Compare the two counts the panel computes. The example deliberately puts eight points in the strip, so testing every pair there would take 28 comparisons. The run does far fewer because each point stops as soon as the next point up is more than d away. That early stop is the entire reason the combine step is cheap.
Someone implements the closest pair method but sorts the strip points bottom to top from scratch inside every single combine step. The answers are still correct. What has it cost?
Correct, and slower by a factor of about log n. The strip can hold most of the points, so sorting it is not free, and it happens at every level of the recursion. The standard fix is to sort all the points by height once at the start and carry that order down through the recursion, so each combine step can pick out its strip in order without sorting again. Getting the right answer and getting the intended running time are two separate pieces of work, and this is a clean example of the second one being the harder half.

What the first twelve steps gave you

  • Use randomness as a design tool, not a last resort: remove the worst case by removing the thing an opponent could aim at.
  • Choose an error probability the way you choose any other budget, and know which of the two answers a one-sided test allows you to doubt.
  • Build a rolling fingerprint, and pair a cheap filter with an exact confirmation.
  • Size a Bloom filter for a target false-positive rate and defend the memory it saves.
  • Argue that a sequence of operations is cheap on average even when one of them is expensive.
  • Tell an approximation with a guarantee from a heuristic that usually works, and say why the difference matters.
  • Model a problem as maximum flow, run Ford-Fulkerson, and read the minimum cut back as an explanation someone can act on.
  • Recognise a linear program when you see one, and know why its answer sits at a corner.

Connections from the first twelve steps

  • Advanced Data Structures. The amortised argument from Step 5 is the everyday tool there: splay trees, union-find and Fibonacci heaps are all cheap only when you average over a run.
  • Computational Complexity. The family of problems in Steps 6 and 7 has a name and a theory, including why a fast method for one would be a fast method for all of them.
  • Optimisation. The simplex method walked over in Step 11, integer programming, and what happens when the corners are too numerous to visit.
Step 13

Write the problem contract before choosing an algorithm

A problem statement needs an input, valid outputs, an objective and limits. “Plan the deliveries” is not enough. May a vehicle revisit a street? Are capacities hard limits? Is the goal shortest distance, earliest completion, fewest vehicles, or a weighted mixture? Two versions that sound alike can require different algorithms and prove different things.

Also name the output type. A decision problem asks yes or no. A search problem asks for a witness. An optimisation problem asks for the best feasible witness. A counting version asks how many witnesses exist. A solver cannot be tested until those contracts and boundary cases are explicit.

feasible solutionAn output satisfying every constraint.
objectiveThe value to minimise or maximise.
witnessConcrete evidence for a yes answer.
Lab 25 · Change the requested output
Try this firstSwitch from decision to search, optimise and count. Read how the required output and its evidence change while the route data stays the same.
Constraints are not cleanup

Release times, negative weights, duplicate objects, disconnected graphs and integer-only variables can invalidate an otherwise correct method. Put each one in the instance model before proving anything.

A route finder returns a path shorter than every legal route, but it exceeds the vehicle capacity. What has it returned?
Feasibility comes first. An objective value is meaningful only among outputs that obey every constraint.
Step 14

Reduce a new problem to a solver you already trust

A reduction translates instances and answers while preserving the question that matters. Job assignment became flow earlier in this course. Difference constraints become shortest paths. Boolean choices can become SAT clauses, and linear objectives with linear constraints can become LP or integer programming models.

The translation needs two proofs. Every original solution must map to a legal target solution, and every target answer you accept must decode to a legal original answer with the promised value. Include the encoding size: a translation that expands n items into 2n clauses is not a polynomial-time reduction.

reductionA correctness-preserving translation between problems.
soundnessAccepted target answers decode to valid source answers.
completenessEvery required source answer remains representable.
Lab 26 · Route three models
Try this firstChoose assignment, difference constraints and Boolean choices. For each, name the target solver and the answer that must be decoded.
Reductions can preserve approximation too

An approximation-preserving reduction tracks how target-solution quality becomes source-solution quality. A yes/no reduction alone may lose the objective information needed for that claim.

What must a reduction preserve besides yes and no?
Translation cost and decoding are part of the proof.
Step 15

Branch and bound searches exactly while proving branches useless

Backtracking explores a tree of partial choices and rejects a branch as soon as it violates a constraint. Branch and bound adds an optimistic bound on the best answer any completion of that branch could reach. If the bound cannot beat the best complete solution already found, prune the whole branch.

The bound must be optimistic in the correct direction. A minimisation lower bound may be smaller than the true completion cost, never larger. Better starting solutions and tighter bounds reduce the tree, but the worst case can remain exponential. Report timeout and optimality gap rather than calling an interrupted incumbent “optimal”.

incumbentBest complete feasible solution found so far.
lower boundOptimistic minimum possible below a branch.
optimality gapDistance between incumbent and global bound.
Lab 27 · Tighten the bound
Try this firstMove bound strength from 0 to 10. Watch the explored tree shrink, then find the point where the lower bound meets cost 105.
Node order changes when a good incumbent arrives

Exploring promising branches first can find a strong incumbent early. That does not change correctness, but it lets the same lower bound prune more of the remaining tree.

For minimisation, when may a branch be pruned?
The branch cannot improve the incumbent. That comparison is a proof, not a guess.
Step 16

Move the explosion into a small parameter

An algorithm with running time nk is polynomial for every fixed k, but it becomes unusable quickly as k grows. A fixed-parameter tractable algorithm has the form f(k)·nc, where c does not depend on k. If k stays small in the intended workload, 2kn can beat nk by an enormous margin.

Choose a parameter that describes the difficult structure: solution size, treewidth, number of errors, or distance from an easy case. Kernelisation safely shrinks the instance to a size bounded by k before the expensive phase. A large or uncontrolled parameter gives no practical rescue.

parameter kA measured part of the instance expected to stay small.
FPTRunning time f(k) times a fixed polynomial in n.
kernelAn equivalent instance whose size is bounded by the parameter.
Lab 28 · Compare n^k with 2^k n
Try this firstSet n to 40 and k to 4. Then raise each slider separately and compare which expression reacts most sharply.
XP is not FPT

Running time nf(k) belongs to the class XP. It is polynomial for each fixed k, but the polynomial degree grows with the parameter. FPT keeps that degree constant.

What distinguishes f(k)n³ from n^k?
The polynomial exponent stays fixed.
Step 17

Heuristics need baselines, restarts and honest claims

Local search starts from one feasible answer and moves to a neighbour that improves it. It can find good routes, schedules and layouts quickly, then stop at a local optimum that is not globally best. Random restarts, tabu memory, simulated annealing and larger neighbourhoods change the exploration; none creates an approximation guarantee by itself.

Evaluate a heuristic against a simple baseline, exact optima on small cases and the best valid lower or upper bound on larger cases. Publish the seed, time budget, machine-independent work budget and full distribution across instances. One best run hides instability.

local optimumNo allowed neighbouring move improves the current answer.
restartRun again from a different initial solution.
benchmark setDeclared instances used for comparable evaluation.
Lab 29 · Compare heuristic evidence
Try this firstCompare one run with twenty restarts. Then add the independent lower bound and see which claim becomes justified.
Keep tuning and evaluation instances separate

If parameters are chosen on the same cases used for the final score, the benchmark becomes part of the training process. Hold out evaluation cases and report every failure.

Twenty restarts found cost 104; a proved lower bound is 100. What may you claim?
The instance-specific bound supports that gap. It says nothing universal about other inputs.
Step 18

Return a solution, its value and evidence

Recompute feasibility and objective value from the returned object instead of trusting cached totals. Then seek independent evidence: a cut matching a flow, dual variables matching a linear-program value, a lower bound matching a branch-and-bound incumbent, or a witness that a decision answer is yes.

Small exhaustive oracles catch modelling and implementation errors. Metamorphic tests transform an input while predicting how the answer changes. Keep solver version, random seed, limits and status with every run so “optimal”, “feasible”, “infeasible” and “timed out” never collapse into one vague success flag.

certificateEvidence that can be checked more simply than rediscovering the answer.
dual boundA bound obtained from a paired optimisation problem.
solver statusExact reason the run stopped and what it proved.
Lab 30 · Audit four solver reports
Try this firstSwitch among feasible, optimal, timed out and infeasible. Identify the extra evidence required before each stronger status is allowed.
No answer and proved infeasible are different

A failed search may mean timeout, memory exhaustion or a bug. Infeasible is a mathematical claim and needs a complete proof procedure or a separately checkable certificate.

What you can do now

You can formulate a problem, classify the requested output, choose exact, randomised, approximate, parameterised or heuristic work, model flow and linear programs, and state what the returned evidence proves. Continue with Computability and Complexity, Optimisation, or Online Algorithms for the corresponding limits and extensions.

A solver stops at its time limit with a feasible cost 120 and a valid lower bound 100. Which status is honest?
The run found an answer but did not close the proof gap.