2.3 Longest increasing subsequence

Again we are given an array of integers. We want to compute the (or rather: a) longest increasing subsequence. A subsequence of a sequence a=[a1,a2,,an] is a sequence of the form b=[aj1,aj2,,ajk] with 1j1<j2<<jkn. More intuitively: a sequence that can be formed by deleting entries in a. The subsequence b is increasing if

aj1<aj2<aj3<<ajk .

We write ISS for increasing subsequence and LISS for longest increasing subsequence. Here is an example:

So [8,9,11,12] is an ISS (increasing subsequence), albeit not a longest one. That would be this one:

Longest increasing subsequence differs from the previous two problems in that even the most trivial (and inefficient) algorithm is not completely trivial.

Exercise 2.3.1 Write test cases as described in Chapter 2.1

An exponential algorithm

Here is a simple recursive idea: let A=[a0,,an1] be the input array. We set B=[a0,,an2] and x=an1. There are two cases: (1) there is a LISS of A that does not contain the last element x; in this case LISS(A)=LISS(B). (2) every LISS of A contains the last index; then it must be of the form S+[x] with S being a LISS of B. But this means that all elements of S (in particular the last one) are strictly less than x. This suggests the following algorithm:

  1. Compute LISS(B).
  2. Compute the longest increasing subsequence S of B whose maximum entry is less than x; then append x.
  3. Take the better of the two.

Note that in Point 2, we have to answer a more general question: what is the longest increasing subsequence with a specified upper bound.

Problem 2.3.2 Implement the above idea. Specifically, implement a recursive function longest_increasing_subsequence_less_than(array, upper_bound). You can then find the overall longest increasing subsequence by calling it with upper_bound being +.

A quadratic algorithm

The previous idea was inefficient because a call to longest_increasing_subsequence_less_than(array, inf) might entail an exponential number of recursive calls. But how many different input arguments (array, upper_bound) will the function encounter? There are at most n+1 possibilities for array, since each will be a prefix of the original input array, i.e., will be of the form input_array[0:j]. The variabel upper_bound is + at the beginning, but afterwards only values occurring in the input value will be passed as upper_bound; thus, at most n+1 values will ever occur, and the total number of combinations (array, upper_bound) is at most (n+1)2. We could just store function calls in a dictionary and will end up with a (somewhat) quadratic algorithm. However, the right way to do things in such a case is to use dynamic programming to search the possibilities systematically.

To actually implement it, we turn things on their head, in a way. Rather than computing, for each index j and upper bound θ the longest increasing subsequence of A[0:j] all whose elements are <θ, we do the following: for each index j and desired length k, compute, among all increasing subsequences b in A[0:j] that have length k, the one whose maximum entry max(b) is smallest. That is,

Tk(j):=min{max(b) | b is ISS of A[0:j],b has length k} .

Note that if A[0:j] does have any ISS of length k, then we take a minimum over the empty set and thus Tk(j) would be empty. On the other hand, for j=0 the array A[0:0] is the empty array, which has only one increasing subsequence: the empty sequence itself; the maximum of the empty sequence is the maximum over an empty set, thus it is , and therefore T0(0)=0. Another insight: it holds that T0(j)T1(j)T2(j). This is because an ISS of length k+1 contains an ISS of length k, so the definition of T(j)k takes a minimum over a potentially larger set than T(j)k+1, giving a smaller or equal value.

How to compute the values of T(j+1) when given T(j)? Let x:=A[j]. Let k be such that

(1)Tk1(j)<xTk(j) .

Since T0(j)= and Tn(j)=+ for all jn1, and the Tl(j) are increasing in l, there is one unique such k. To build an ISS of length k in A[0:j], we can take an ISS of A[0:j] of length k1 ending in Tk1(j), append x, and obtain an ISS of A[0:j+1] of length k ending in x. Or we could just take an ISS of length k in A[0:j]. Either way, it holds that

Tk(j+1)=min(x,Tk(j)) .

What about l>k? Since Tl(j)Tk(j)x, x cannot be appended to any ISS in A[0:j] of length l; it would not be an increasing sequence anymore. Therefore:

Tl(j+1)=Tl(j) for all lk+1 .

What about lk1? Take an ISS of length l1 in A[0:j]. Maybe we can append x to form an ISS of length l ending in x; but we already have one ending in Tl(j)Tk1(j)<x, so appending x would just give a worse (i.e., higher-ending) ISS than we already have. The row j+1 thus differs from row j in at most one position. To summarize:

Tk(j+1)=min(x,Tk(j)) for k such that Tk1(j)<xTk(j),Tl(j+1)=Tl(j) for all lk.

In the example run below, we don't keep row for every j. We just show the values Tk(j) for the current j and cross out numbers that have been replaced by better ones.

Once we have processed all elements, we have arrived at Tk(n), which is the smallest possible value a ISS of size k can end with. The largest k for which this is not + is the length of a LISS.

Problem 2.3.3 Implement the idea that we just outlined. Start with a program that computes the length of the longest increasing subsequence, not the sequence itself.

Computing the sequence itself

We want our program to output a longest sequence, not just its length. To facilitate judging the correctness, the answer should be unique, so we have to come up with tie breaking rules: when two increasing subsequences b1<b2<<bk and c1<c2<<ck have the same size, we prefer one with the smallest values. But what does that even mean? Couldn't it be that one LISS is [4,5,21] and the other is [7,8,19]? In that case, which one has "smaller values"? It turns out that there is always a unique minimum that is minimal in every position:

Theorem 2.3.1 Suppose [b1,,bk] and [c1,,ck] are both LISS of the array A. Then

min(b1,c1),min(b2,c2),,min(bk,ck)

is also a LISS of A.

Remark: It is pretty easy to show that the new sequence is increasing. In fact, this holds even if b and a are not necessarily longest ISS of A. However, it might be that the new sequence is not a subsequence of A! For example, when A=[2,3,1,4] and a=[2,3] and b=[1,4] then the pointwise minimum would be [1,3], but now this is not a subsequence of A anymore. To prove that this cannot occur, you must use that a and b are, in fact, longest increasing subsequences of A.

Problem 2.3.4 Add bookkeeping to your implementation such that it does not only output the length of the LISS but the LISS with minimal values itself.

python quadratic.py < testcases/example-from-lecture-notes.in                             
6
4 6 7 8 11 12

Problem 2.3.5 Improve your implementation of the quadratic algorithm: in order to find the k for which (1) holds, don't use linear search; use binary search.