最长上升子序列的最优算法
最长上升子序列的最优算法
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]满足一下情况.
- x < y < t
- A[x] < A[y] < A[t]
- F[x] = F[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[i]的值是在整个计算过程中是单调下降的。
- D[]的值是有序的,即D[k] < D[k + 1]。
利用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]。
- 如果A[j + 1] > D[len],根据归纳假设,A[1],A[2],··· ,A[j]中最长上升子序列的长度为len,且所有的长度为len的上升子序列的最后一个数的最小值为D[len].此时我们把A[j + 1]接在A[1],A[2],··· ,A[j]中以D[len]为结尾且长度为len的单调递增子序列的末尾。此时的到的序列的长度为len + 1,而且是A[1],A[2],··· ,A[j + 1]的上升子序列。又由于A[1],A[2],··· ,A[j]的最长上升子序列的长度为len,所以A[1],A[2],··· ,A[j + 1]的最长上升子序列的长度不会超过len + 1.而根据我们的构造得到了一个长度为len +
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的上升序列的的最后一个数的最小值这个命题时成立。
- 如果A[j + 1] < D[len],根据归纳假设,A[1],A[2],··· ,A[j]的最长上升子序列的长度为len,且所有的长度为len的上升子序列的最后一个数的最小值为D[len]。现在我们来证明A[1],A[2],··· ,A[j + 1]最长上升子序列的长度不可能是len + 1,这里我们采用反证法来证明。由于len是原序列中的最长上升子序列的长度,如果A[1],A[2],··· ,A[j +1]最长上升子序列的长度是len + 1,则A[j + 1]一定是这个最长上升子序列的末尾。而又由于在A[1],A[2],··· ,A[j]中所有的长度为len的上升子序列的最后一个数的最小值为D[len],且A[j + 1] < D[len],所以这个所谓的子序列并不是上升子序列。因此, A[1],A[2],··· ,A[j + 1]最长上升子序列的长度不可能是len + 1,len不需要更新处理。但是D[]中的元素可能需要更新为A[j + 1],为此我们首先在D[1],D[2],··· ,D[len]中寻找最小的i,使得D[i] > A[j + 1].由于性质2,我们只能更新一个D[k],因为如果更新两个的话,D[]会有两个值为A[j + 1],就不是一个单调递增序列了。而且根据性质1,
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)的时间内找到一个最长上升子序列,方法如下。
- 初始化阶段:我们从右到左遍历数组来找到A[i] = D[len]的i,然后用一个链表L从头部插入A[i]。
- 遍历阶段:从右向左继续遍历来找到一个j,使得F[j] = F[i] − 1且A[j] < A[i],将A[j]从头端插入链表L,然后更新i = j。
- 并判断F[i] = 1是否成立,如果成立则进入输出阶段,否则继续迭代遍历.
- 输出阶段:从头到尾遍历输出L。