Run it
Both versions, agreeing on the length.
Whether tails is the subsequence - checked on two inputs rather than claimed on one.
The predecessor-tracking version fixes that by rebuilding the actual subsequence, not just its length.
The cost that separates them, and the one call that changes the question.
Switching bisect_left for bisect_right is the whole difference between strictly increasing and non-decreasing runs.
The quadratic version first
best[i] = the length of the longest increasing subsequence ending at index i. To compute it, look at every j < i with a[j] < a[i] and take the best of those, plus one. The answer is the maximum over all best[i].
It is O(n²), it is four lines, and it is the right thing to write first. The "ending at i" framing is the part worth saying aloud: without it people try to define best[i] as "the answer for the first i elements", which does not admit a recurrence, because you cannot tell whether the run can be extended.
What the tails array actually holds
This is the part that is usually recited without being understood. tails[k] is the smallest value that can end an increasing subsequence of length k+1, among everything seen so far.
Two facts follow. It is sorted, because a longer run must end higher than the best ending of a shorter one — which is what makes the binary search legal. And overwriting an entry never breaks anything: replacing a tail with a smaller value keeps the same achievable length while making future extensions easier.
So each element does one of two things. If it exceeds every tail, it extends the longest run and the array grows. Otherwise it replaces the first tail that is not smaller than it — bisect_left — and the length is unchanged.
The trap: tails is not the answer
Run it on [2, 6, 8, 3, 4, 5, 1] and the final tails array is [1, 3, 4, 5]. The length, 4, is correct. The contents are not a subsequence of the input at all — the 1 is the last element, so it cannot precede the 3. The real answer is [2, 3, 4, 5].
What makes this trap dangerous is that tails often is a valid subsequence by coincidence: on [10, 9, 2, 5, 3, 7, 101, 18] it comes out as [2, 3, 7, 18], which is genuinely increasing and genuinely a subsequence. Checking one example proves nothing here. If the actual subsequence is wanted, keep a predecessor index for each element as it is placed and walk the chain backwards — the editor below does that, and checks both against the input rather than asserting.
Strict, non-strict, and the follow-ups
bisect_left gives strictly increasing: an equal value replaces rather than extends. bisect_right gives non-decreasing, where equal values extend the run. One function call, two different problems — and the problem statement often does not say which it wants.
The name to know is patience sorting, after the card game: each tail is the top card of a pile, and you place each new card on the leftmost pile whose top is not smaller. The number of piles is the answer. It is also the basis of the difflib patience diff, which is a good thing to be able to mention.
Edge cases to raise
Volunteering these is most of what separates a correct answer from a good one.
Empty input. Length 0, and the quadratic version's max(best) raises on an empty list.
All equal values. Strictly increasing gives 1, non-decreasing gives n. Which one the problem wants is frequently unstated.
Strictly decreasing input. The answer is 1 and the tails array is length 1, overwritten every step - the case worth tracing if the replacement logic looks suspicious.