1. Longest Increasing Subsequence
Some of this material is borrowed from Jeff Erickson’s chapters on Recursion, Backtracking, and Dynamic Programming. In particular, the (very nice) analogy with traffic lights is from Erickson. Check them out for a more detailed comparison between recursive and memoized implementations. Thanks to Arya for pointing out a correction.
The Problem
For any sequence , a subsequence of is another sequence obtained from by deleting zero or more elements, without changing the order of the remaining elements; the elements of the subsequence need not be contiguous in .
For example, when you drive down a major street in any city, you drive through a sequence of intersections with traffic lights, but you only have to stop at a subsequence of those intersections, where the traffic lights are red. If you’re very lucky, you never stop at all: the empty sequence is a subsequence of . On the other hand, if you’re very unlucky, you may have to stop at every intersection: is a subsequence of itself.
Here are some examples:
BOAT
is a subsequence ofBOX-OF-CHOCOLATES
.OOF
is a subsequence ofBOX-OF-CHOCOLATES
, butOO-F
is not.O-O-OOLA
is a subsequence ofBOX-OF-CHOCOLATES
, butFOX
is not.FOOL
andBHOLA
are a subsequences ofBOX-OF-CHOCOLATES
, butLAX
andBETA
are not.
Now suppose we are given a sequence of integers, and we need to find the length of a longest subsequence whose elements are in increasing order:
- the input is an integer array , and
- we need to compute the length of a longest possible sequence of indices such that for all .
We will use LIS
to refer to the phrase longest increasing subsequence. Note that there may be multiple increasing subsequences, and even multiple choices for increasing subsequences that are the longest. The approaches below that work correctly to find the length of a LIS are easily adaptable to finding a LIS as well.
The Solution — Take 0 (a dead end)
The Solution — Take 1 (building on the bad take)
The Solution — Take 2 (a different approach)
Time and Space Complexity
For the first approach:
- We filled out entries at cost each, so the overall running time of the algorithm is .
- We store entries, although with a careful implementation the storage can be limited to .
For the second approach:
- We filled out entries at cost each, so the overall running time of the algorithm is again.
- We store values.
Correctness
The correctness of the computations of the -values specified by both approaches above is implicit in the design of the recurrence. However, we provide an explicit proof for the second approach here. The proof of correctness for most of the other recurrences that we will discuss can be established with a similar approach in spirit and is left as an exercise.
Theorem. Let be given by:
and let denote the length of a LIS in . Then .
Proof.
Part 1. The DP stores a value that is achievable.
First, let us show that , that is, there is an subsequence of length that begins at . We prove this by backward induction over .
The base case. , and this is witnessed by the subsequence .
The induction step. If , then and this is witnessed by the subsequence .
Otherwise, Let be the index that witnesses the value (if there are multiple indices that fit this bill, choose any). Note that . By the induction hypothesis, there is an subsequence of length that begins at . Prepend to this subsequence the value , this is clearly an increasing subsequence of length , and we are done.
Part 2. The value computed by the DP cannot be beaten.
First, let us show that , that is, the length of any longest subsequence in is at most . We prove this by backward induction over .
The base case. , and , so there is nothing to prove.
The induction step. Here we proceed by contradiction. Suppose there is a subsequence (say ) in whose length is .
Consider the tail of , that is, the entire subsequence with the first value — i.e, — omitted. Denote this subsequence by .
Note that has elements. Suppose the first element of is . Then . Note also that and , so (by the induction hypothesis). Thus:
a contradiction.
Footnotes
Indeed, since we already have a kickstart at the fragment , if we can figure out a way of recovering the value of assuming knowledge of for , then we are done: we can use this method to go backwards starting from all the way back to .↩︎