The Patterns Behind Most Coding Problems
What a Pattern Is Actually Exploiting, and Why It Is Never Cleverness
A fast method is one piece of structure in the input being used to rule out candidates unexamined. Six structures are worth naming, and an invariant is what licenses not looking.
Every fast method looks like a trick the first time it is read and like an
inevitability the second. What changes between the two readings is not the
code. It is knowing what the method was allowed to assume.
So here is a list, and it is the same list throughout.
$$
Ten values, summing to 64, the largest of them 15. And one question about it.
How many pairs of its values sum to 18?
The baseline, which is a number and not an insult
Try every pair. Each of the ten values could be the first member of a pair and
each of the nine others the second, which is ordered pairs,
and every unordered pair has been counted twice, once in each order, so there
are pairs. Forty five is the whole of the work, and it is worth
writing down before anything else, because every method below is measured
against it.
Two of the 45 sum to 18. They are and .
| step | pair | its total | pairs tested | pairs summing to 18 | what happened |
|---|---|---|---|---|---|
| 1 | 2 and 7 | 9 | 1 | 0 | the first value against the second |
| 2 | 2 and 15 | 17 | 3 | 0 | one short of the target, which tells us nothing at all |
| 3 | 2 and 3 | 5 | 9 | 0 | the first value is finished and nine pairs are gone |
| 4 | 7 and 11 | 18 | 10 | 1 | the first hit, at pair ten of forty five |
| 5 | 15 and 3 | 18 | 30 | 2 | the second hit, twenty pairs later |
| 6 | 9 and 3 | 12 | 45 | 2 | the last pair, and only now is the count safe to report |
- the number of values in the list
- the number of unordered pairs, which is how many candidates the pair question has
The candidate space is what gets searched, not the input
The list has ten values. The pair question has 45 candidates. Those are
different numbers, and the second one is the one that decides the cost.
Change the question and the count moves a long way without the input moving at
all. A question about contiguous stretches of has
candidates, counting each single value as a stretch of one. A question about
which values to keep and which to drop has non-empty
subsets. A question about the order to process them in has
orderings. One list of ten numbers, and the search in front of you is 45 or 55
or 1,023 or 3,628,800 depending on nothing but the sentence that was asked.
So the first move on an unfamiliar problem is not to recall a method. It is to
say what a candidate is, count them, price one check, and multiply. That
product is the baseline, and it answers a question that growth shapes cannot:
whether a faster method is needed at all. A baseline of a million steps is
finished before anyone has read the second paragraph of the statement, and
replacing it with something clever is a way of introducing a bug. A baseline of
57.87 days is not going to be rescued by tuning.
The six structures
When the baseline is too slow, something in the input has to be used. There are
about six things worth naming, and they are the reason the handful of patterns
that follow exist at all.
| Structure | What it says about the input | What one comparison can then rule out | The giveaway in the statement |
|---|---|---|---|
| Sorted order | the values are in order, or may be put in order | everything on one side of the value compared | sorted, ordered, ranked, non-decreasing |
| Adjacency | the answer is a run of neighbours rather than any subset | every stretch that is not an extension of the current one | contiguous, consecutive, substring, window |
| A small answer set | only a few items out of many are wanted | everything worse than the worst item currently kept | top k, k largest, best few, median |
| A cheap check | testing a proposed answer is easier than finding one | half of the remaining candidate answers | minimise the maximum, smallest capacity such that |
| Repeated subproblems | the same smaller question is asked many times over | every repeat of a subproblem after the first | in how many ways, optimal, overlapping choices |
| A bounded alphabet | values come from a small fixed set | the distinction between items that share a value | lowercase letters, digits, bytes, 0 to 255 |
Two of these are worth testing against the list above straight away, because
they cut the same 45 in different ways and by different amounts.
Sorted order. Put in order to get
and compare the two ends. ,
which is below 18. The low end 1 cannot reach 18 with any partner at or below
15, which is every partner it has, so 1 is finished and nine pairs are gone in
one comparison. Then , still below, and eight more pairs go. Then
, which is a hit. Three comparisons, of which the first two ruled
out of the 45 pairs without looking at any of them, while the
third landed on the answer itself.
A bounded alphabet. The list says nothing about its alphabet, so this one is
not available here. Had the statement said the values were bytes, the most
common value question would stop being about pairs or order at all: 256
counters, one pass, and the sortedness nobody has would be irrelevant.
What an invariant actually is
The sorted sweep above reported an answer after examining three of 45 pairs.
That is a claim about 42 pairs it never touched, and the claim has to live
somewhere. The place it lives is the invariant.
An invariant is not a comment and not a true remark about a loop. It is a
statement that passes three separate checks.
It holds before the first step. It is preserved by every step, so that if it
held going in it holds coming out. And at the stopping condition it is strong
enough to force the answer.
Take the exhaustive search first, because the invariant is easiest to see where
it is least needed. Say that before pair number is examined, the running
count holds the number of pairs among the first that sum to 18. Before
the loop, and no pairs have been examined and the count is 0, so it
holds. Examining pair adds one to the count exactly when that pair sums to
18, so the statement holds again with one larger. The loop stops when
, at which point the statement says the count holds the number of hits
among all 45 pairs, which is the answer. Three checks, three sentences.
Now the part that gets skipped. The third check is a real check and it fails
for statements that pass the first two. The count of hits is at most 45 before
the loop, is still at most 45 after every single step, and is at most 45 at the
end. It is an invariant in the sense that it never varies, and at the stopping
condition it says nothing whatever about the answer. An invariant that survives
the loop without pinning the answer is a true sentence, not a proof, and the
only way to notice the difference is to do the third check explicitly.
def count_pairs_summing_to(xs, target):
hits = comparisons = 0
for i in range(len(xs)):
for j in range(i + 1, len(xs)):
comparisons += 1
if xs[i] + xs[j] == target:
hits += 1
return hits, comparisons # (2, 45) on A with target 18
def any_pair_summing_to(ordered, target):
i, j, comparisons = 0, len(ordered) - 1, 0
while i < j:
comparisons += 1
total = ordered[i] + ordered[j]
if total == target:
return (ordered[i], ordered[j]), comparisons
if total < target:
i += 1 # no partner at or below ordered[j] lifts this low end
else:
j -= 1 # no partner at or above ordered[i] lowers this high end
return None, comparisons # ((3, 15), 3) on S with target 18The structure is not free, and on ten values it loses
Here is the step that gets left out of every short version of this subject. The
sweep needs a sorted list, and is not sorted. Sorting ten values by
comparisons costs about
comparisons. Add the three the sweep uses and the sorted route costs
comparisons against 45 for trying every pair, and against the ten
it actually took to stumble on . For one existence question on ten
unsorted values, the pattern is slower than the thing it replaces.
That is not an argument against the pattern. It is the statement of when the
pattern applies. Structure is free when it arrives with the data, and cheap
when it is paid for once and then asked many questions, and expensive when it
is built to answer one. The arithmetic flips entirely with scale. At
the sort costs about
comparisons and the sweep
costs at most more, against 4,999,999,950,000,000 for every
pair. The sort is now roughly two million times cheaper than the thing it
enables you to skip.
| n = 10 | n = 1000 | n = 100,000,000 | |
|---|---|---|---|
| every pair | 1.65 | 5.70 | 15.70 |
| sort, then one sweep | 1.52 | 4.00 | 9.42 |
| one pass | 1.00 | 3.00 | 8.00 |
| halving what is left | 0.52 | 1.00 | 1.42 |
Reading the structure off the statement
The procedure, in the order the steps actually help.
Say what a candidate is and count them. Price one check. Multiply, and look at
the number in seconds, because sometimes it ends there. If it does not,
underline what the statement promises about the input and match it against the
six structures above. Then ask the only question that produces a method, which
is what a single comparison would be entitled to rule out given that promise.
If the answer is one candidate, there is no pattern to be had and the baseline
stands. If the answer is a block of candidates whose size grows with , the
method writes itself from the invariant that justifies the discard.
That last clause is the one worth distrusting until it has been done in full.
The sorted sweep was asserted above, not proved, and the proof is a single
inequality applied exactly once. It is worth doing properly, because every
pattern that follows is the same manoeuvre with a different structure under it.
Recap
- A fast method is never cleverness, it is one piece of structure in the input being used to rule out candidates without examining them, and naming that structure is the whole of the work.
- The cost of the exhaustive method is the size of the candidate space multiplied by the cost of one check, so a ten value list already carries 45 pairs, 1,023 non-empty subsets and 3,628,800 orderings, and counting the candidates comes before choosing a method.
- A statement earns the name invariant only when it is true before the first step, preserved by every step, and strong enough at the stopping condition to force the answer, and the third part is the one that gets skipped.
This is the reading half
Starting the course gives you your own copy of it. Every idea on every page has problems standing under it, marked with a reason rather than a tick, and any sentence you do not believe can be opened and argued with. None of that can happen on a page nobody owns.
The contents