Learn
Lesson · Dynamic Programming and Optimal Substructure · Sequences, and counting rather than optimising

The Tails Array Is Not the Subsequence

Two algorithms find the same length. The quadratic table stores, for each position, the longest increasing run ending there, and a predecessor, so the run itself can be read back. The binary-search method keeps the smallest possible last value for each length and finds the same number far faster — and what it holds at the end is not a subsequence of the input, though on the sequence in front of you it happens to be one.

table: best(i) = 1 + max of best(j) over j < i with a(j) < a(i) tails: tails(L) = the smallest value that can end an increasing run of length L 10 9 2 5 3 7 101 18 4 8 the table length 4 2 5 7 101 45 comparisons the tails length 4 2 3 4 8 14 comparisons every subsequence length 4 1024 examined
Two routes to one length, and two different arrays at the end of them

Core ideas

What this lesson establishes

The hard idea is the invariant the tails array maintains, which is about lengths and not about any particular run, and which is why its contents need not be a run at all.

1

The table stores an answer per position, and a predecessor

best(i) is the length of the longest increasing subsequence ending exactly at position i. It is one more than the largest best(j) over earlier positions holding smaller values, and recording which j attained that maximum makes the subsequence recoverable. The whole array takes Θ(n²) comparisons — 45 on ten values — and the answer is the largest entry.

2

The tails array stores a bound per length, not a run

tails(L) is the smallest value that can end an increasing subsequence of length L among the elements seen so far. Each new value replaces the first entry that is at least as large, or extends the array. The invariant is about what is achievable at each length; nothing says the entries occur in the input in that order, and usually they do not.

3

Two properties, checked separately, and one of them is the trap

An answer must be increasing and must be a subsequence of the input. The tails array is always increasing — that is forced by the invariant — so checking only that property will never catch it. On the lab's default sequence it also happens to be a subsequence, which is worse: the check passes and the array still is not the answer the table found. Switch to the sequence with repeats and the subsequence check fails outright.

The material

Two algorithms, one length, and the array that is not an answer

The quadratic table, the binary-search invariant, the two checks, and the instance where agreement is a coincidence.

Definition · Longest increasing subsequence

A subsequence of a sequence is what is left after deleting zero or more entries, keeping the rest in order. It is strictly increasing if each entry is larger than the one before. The problem is to find a longest strictly increasing subsequence; there may be many, and the lesson is careful never to call one of them the answer.

Theorem · The per-position recurrence

Let best(i) be the length of the longest strictly increasing subsequence ending at position i. Then best(i) = 1 if no earlier position holds a smaller value, and otherwise best(i) = 1 + max of best(j) over positions j < i with a(j) < a(i). The answer is the maximum over all i.

A longest increasing subsequence ending at i has some second-to-last entry, at a position j < i with a(j) < a(i), unless it consists of a(i) alone. Removing a(i) leaves an increasing subsequence ending at j, and it must be a longest one, or substituting a longer one and re-appending a(i) would beat the original.

So the length is 1 + best(j) for that j, and taking the maximum over all admissible j gives best(i). Every increasing subsequence ends somewhere, so the overall answer is the largest best(i), and the predecessor recorded at each step reconstructs one witness for it.

□

What the other algorithm maintains

The binary-search method processes the values in order and keeps an array tails in which position L holds the smallest value that ends an increasing subsequence of length L + 1 among the values processed so far. A new value either extends the array, when it is larger than everything in it, or replaces the first entry that is at least as large. Both cases are found by binary search, so the whole run is Θ(n log n) comparisons — 14 on ten values, against the table's 45.

Nothing in that description promises the array is a subsequence of the input. It is a record of what is achievable at each length, and its entries may come from positions in any order, including positions that cannot coexist in one run. The length is right, because the array's length is exactly the longest achievable, and the contents are a different question.

Example · Ten values, and the array that changed its mind

On 10 9 2 5 3 7 101 18 4 8 the tails array after the seventh value is 2 3 7 101, which is a genuine longest increasing subsequence. Then 18 replaces 101, then 4 replaces 7, then 8 replaces 18, and the array ends as 2 3 4 8. The table's reconstruction is 2 5 7 101. Both have length 4 and both are valid answers here; the array passed through one valid answer on its way to a different one, and nothing about the mechanism guarantees the destination is valid.

That last sentence is the whole lesson, and the lab has the instance that proves it. Switch the sequence to 3 1 4 1 5 9 2 6 5 3. The tails array ends 1 2 3 6, which is increasing; the panel's second check asks whether it is a subsequence of the input, and the answer is no — 1, 2 and 3 occur in that order, and the only 6 sits before the last 3. The table's reconstruction is 3 4 5 9, which passes both checks.

Why two checks, and why the weaker one never fires

The panel reports is increasing and is a subsequence of the input as separate verdicts. The first is satisfied by the tails array on every input, because the invariant forces it, and by a sorted copy of the input too — which is why it cannot be the only check. The second is the one with teeth. A reconstruction that returns the right values in the wrong positions fails it, and so does an array that was never a run in the first place.

The third route is the enumeration: every one of the 2ⁿ subsequences, tested for increase, with the longest kept. On ten values that is 1 024 subsets, which is instant, and the lab refuses above sixteen values. It agrees with both algorithms on the length, which is what makes the comparison of the two arrays a comparison rather than a guess.

The measured comparison counts are 45 and 14 on ten values. The proved bounds are Θ(n²) and Θ(n log n), and the enumeration is Θ(2ⁿ n). Here the three measured numbers and the three bounds agree in order, which is worth saying out loud on a course where three pages show them disagreeing. What does not follow from the measurement is the shape of either curve: 45 and 14 are two numbers, and two numbers fit any pair of curves you like.

Interactive

The right length, and the wrong list

Three routes to the length, and two separate checks on the subsequence

The tails array has the right length and the wrong contentsthree routes to the length, and two checks on the subsequence
in the subsequence the table reconstructsin the tails arrayin neither

Method

Comparing two algorithms that report the same number

Agreement on a number is the weakest kind of agreement, and it is the one that is easiest to mistake for correctness.

1

Ask what each algorithm's array is for

The table's array holds an answer per position; the tails array holds a bound per length. Those are different objects with the same shape, and the shape is what invites the confusion. Write the sentence defining each entry before comparing them.

2

Recover the answer from predecessors, never from the bound array

The subsequence comes out of the table's recorded predecessors. If you need the run itself and you are using the binary-search method, you must record predecessors there too — the tails array alone cannot produce it.

3

Check both properties, not the easy one

Increasing, and a subsequence of the input. The first is satisfied by things that are not answers at all, including a sorted copy of the input, so a check that stops there has not tested anything the failure mode can fail.

4

Try a sequence with repeats before believing a coincidence

On the lab's default the tails array is a valid answer, and on the sequence with repeats it is not even a subsequence. If an invariant does not promise a property, a single input exhibiting the property is a coincidence to be checked against a second input, not evidence.

5

Read the comparison counts as two numbers, not as two curves

Forty-five and fourteen at one length. The bounds Θ(n²) and Θ(n log n) come from the structure of the loops, and no pair of measurements establishes them.

Lengths, arrays, and the property that does the work

Score 0 / 0

The tails array, one value at a time

Each line is one value being processed. Position is where the binary search placed it: past the end extends the array, anywhere else replaces an entry. The input is 10 9 2 5 3 7 101 18 4 8.

value position tails afterwards 10 0 10 9 0 9 2 0 2 5 1 2 5 3 1 2 3 7 2 2 3 7 101 3 2 3 7 101 <- a valid answer, in passing 18 3 2 3 7 18 4 2 2 3 4 18 8 3 2 3 4 8 <- what it ends as the table, meanwhile input 10 9 2 5 3 7 101 18 4 8 best(i) 1 1 1 2 2 3 4 4 3 4 prev(i) - - - 2 2 3 5 5 4 5 largest best is 4, first attained at 101; walking prev back from there gives 2 5 7 101 every one of the 1024 subsequences agrees: the longest has length 4

Only the length is shared. The table's run is 2 5 7 101, occupying positions 3, 4, 6 and 7 of the input; the tails array is 2 3 4 8, whose values occur at positions 3, 5, 9 and 10 and which is also a valid run here. Two valid answers and one number, and the panel says whether the two coincided rather than assuming they must.

Watch the seventh line. At that moment the tails array is 2 3 7 101, a perfectly good answer, and the algorithm then overwrites it three times because smaller endings become available. Each of those replacements is correct for the invariant it maintains — the smallest possible last value for each length — and each of them destroys a run that was there a moment ago. The array is doing its job; its job is not to hold a run.

For a faded rehearsal, switch the sequence to 3 1 4 1 5 9 2 6 5 3 before running it. The supplied first move: the value 1 appears twice and equal values cannot both sit in a strictly increasing run, so the second 1 replaces rather than extends. Predict the final tails array, then check the panel's second verdict — this is the instance where the array is not a subsequence at all.

Common mistakes

1
Printing the tails array as the answerIt has the right length, it is increasing, and on many inputs it is even a valid run — which is exactly why this shipped as a bug in real code more than once. The array is a record of the smallest achievable ending per length. The run comes from predecessors or not at all.
2
Checking only that the output is increasingThe property the failure mode has is increase; the property it lacks is being a subsequence of the input. A check that tests the first and not the second has been written to pass. The panel reports both, separately, for that reason.
3
Concluding from one sequence that the two methods return the same objectThey return the same number on every input and the same object on some. The lab ships four sequences and they differ: on the sorted one the two coincide exactly, on the default one they are two different valid answers, and on the one with repeats the array is not an answer at all.

Check your understanding

✓
Finish when you can say what each of two arrays holds and which property separates them.

You should be able to state the per-position recurrence and the tails invariant in one sentence each, recover a run from predecessors, check a candidate for both required properties, and produce the input on which the faster method's array is not an answer.

Note: Every table so far has optimised something. The next one counts, which changes the recurrence from a minimum to a sum, and makes the order of the two loops decide not how fast the answer arrives but which question was asked.

Kept in this browser only, for you. Nothing is sent anywhere and nothing checks it. Carry it to another device.

Feedback and recommendations

Something to change about this lesson? Write it down, tick it off when it is handled, and export the list when you want to act on it. Kept in this browser only: nothing is sent anywhere, and nobody is notified.

No recommendations yet for this lesson.