最长上升子序列的最优算法
1 问题背景
最长上升子序列问题(Longest Increasing Subsequence)在算法教学中的经典问题,在学习动态规划(Dynamic Programming)相关内容时经常出现。在动态规划这一章节中出现的频率只比最长公共子序列(Longest Common Sequence)小。最长上升子序列问题的动态规划解法的时间复杂度为n2,而我们可以得到的最长上升子序列的最优化算法的复杂度为nlogn。最长上升子序列在我们的课程中是作为动态规划的联系来做的,这样就误导了大家,使得大家认为最长上升子序列问题的最优算法就是动态规划算法。这样就违背另外算法研究的精神,所以我写这篇文章,以正视听。
设A=< x1,x2,··· ,xn >是n个不等的整数构成的序列,A的一个单调递增子序列<
xi1,xi2,··· ,xik >使得i1 < i2 < ··· < ik,且xi1 < xi2 < ··· < xik.子序列< xi1,xi2,··· ,xik >的长度是含有的整数个数k.例如A =< 1,5,3,8,10,6,4,9 >,他的长为4的递增子序列是:
< 1,5,8,10 >,< 1,5,8,9 >,···.设计一个算法求A的一个最长的单调递增子序列,分析算法的时间复杂度.
3 算法思想
设A[t]表示序列中的第t个数F[t]表示从1到tt这一段中以t结尾的最长上升子序列的长度F[t] = 0(t = 1,2,··· ,len(A))。则有动态规划方程F[t] = max1,F[j] + 1(j = 1,2,...,t −
1,且A[j] < A[t]).
现在,我们仔细考虑计算F[t]时的情况。假设有两个元素A[x]且A[y]满足一下情况.
此时,选择A[x]和选择A[y]都可以得到同样的F[t]值,那么,在最长上升子序列的这个位置中,应该选择A[x]还是A[y]
很明显,选择A[x]比选择A[y]要好。因为由于条件(2),在A[x + 1]···A[t − 1]这一段中,如果存在A[z]æA[x] < A[z] < A[y],则与选择A[y]相比,将会得到更长的上升子序列。再根据条件(3),我们会得到一个启示:根据F[]的值进行分类。对于F[]的每一个取值k,我们只需要保留满足F[t] = k的所有A[t]中的最小值。设D[k]记录这个值,即D[k] = minA[t](F[t] = k)。
注意到D[]的两个特点:
利用D[],我们可以得到另外一种计算最长上升子序列长度的方法。设当前已经求出的最长
上升子序列长度为len。我们利用t来遍历A[],如果A[t]¿D[len],则我们将A[t]接在D[len]后面,这样我们就得到了一个更长的上升子序列。此时更新len = len + 1,D[len] = A[t];否则在D[1],D[2],··· ,D[len]中寻找最小的j,使得D[j] > A[t],将D[j]更新为A[t],其他不变。最后遍历完整个A[]后,len就是A[]中最长上升子序列的长度。
4 算法正确性的证明
我们用数学归纳法来证明这个算法的确返回了A[]中最长上升子序列的长度。归纳命题如下:对于1 ≤ t ≤ n,当我们处理完A[t]时,len是A[1],A[2],··· ,A[t]最长上升子序列的长度。且对于1 ≤ i ≤ len,D[i]则是A[1],A[2],··· ,A[t]中所有长度为i的上升序列的的最后一个数的最小值。
归纳基础: 当t = 1时,len = 1,D[1] = A[1]命题显然成立。
归纳假设:如果当t = j时归纳命题成立,则我们现在来处理A[j + 1]。
1的上升子序列,所以该序列是A[1],A[2],··· ,A[j + 1]的最长上升子序列。因此len + 1是A[1],A[2],··· ,A[j + 1]的最长上升子序列的最大长度。我们把len更新为len + 1,同时把D[len]更新为A[j + 1]。而根据性质2,D[i]是不会变大的,所以原始的D[]不会受到任何影响。因此处理完A[j + 1]后,len是A[1],A[2],··· ,A[j + 1]最长上升子序列的长度且对于1 ≤ i ≤ len,D[i]则是A[1],A[2],··· ,A[j + 1]中所有长度为i的上升序列的的最后一个数的最小值这个命题时成立。
D[k]的值是不会变大的,所以我们只能更新k ≥ i的某一个D[k]。同时又由于性质2,我们只能更新D[i],否则的话D[]中会出现逆序对。现在我们证明更新D[i]是正确的,由于D[i − 1] < A[j + 1],所以把A[j + 1]接在以D[i − 1]结尾的最长上升序列的末尾是可行的,这时得到的序列长度为i,且为上升序列.又因为D[i] > A[j + 1],所以我们可以把D[i]更新为A[j +1].此时,处理完A[j +1]后,len是A[1],A[2],··· ,A[j +1]最长上升子序列的长度且对于1 ≤ i ≤ len,D[i]则是A[1],A[2],··· ,A[j + 1]中所有长度为i的上升序列的的最后一个数的最小值这个命题时成立。
根据上面的两种情况的分析,我们可以得出以下结论,当t = j归纳命题成立时,t = j+1命题
也成立。由上面的分析可以推出:对于1 ≤ t ≤ n,当我们处理完A[t]时,len是A[1],A[2],··· ,A[t]最
长上升子序列的长度。且对于1 ≤ i ≤ len,D[i]则是A[1],A[2],··· ,A[t]中所有长度为i的上升序列的的最后一个数的最小值。
在上述算法中,若使用朴素的顺序查找在D[1],··· ,D[len]查找,由于共有O(n)个元素需要计算,每次计算时的复杂度是O(n),则整个算法的时间复杂度为O(n2),与原来的算法相比没有任何进步。但是由于D[]的特点(2),我们在D[]中查找时,可以使用二分查找高效地完成,则整个算法的时间复杂度下降为O(nlogn),有了非常显著的提高。
上面所说的算法可以在O(nlogn)时间内得到A[]的最长上升子序列的长度,但是我们得到的最后的D[]序列一般来说并不是符合条件的最长上升子序列。为了得到一个最长上升子序列,我们需要对原有的算法做一点修正,即用F[t]来记录A[1],··· ,A[t]的最长上升子序列的长度。这个操作可以在每次处理完A[t]之后,进行F[t] = len这个赋值操作来完成。我们可以在O(n)时间内得到F[],在我们得到F[]后,我们可以在O(n)的时间内找到一个最长上升子序列,方法如下。