最长增序列(Longest Increasing Subsequence,LIS )给定一个数列,从中删掉任意若干项剩余的序列叫做它的一个子序列,求它的最长的子序列,满足子序列中的元素是单调递增的。
例如给定序列{1,6,3,5,4},答案是3,因为{1,3,4}和{1,3,5}就是长度最长的两个单增子序列。
C(n,0)+C(n,1)+…+C(n,n)= 2n ,还不算对单个C(n,i)比较。
其实只需要考虑加入的数比之前加入的最后一个数大就可以了。而最长的意思是数的个数最多,我们只要知道数的总个数就可以了,没必要知道具体有哪些数。
假设这个序列是 a1,a2,a3…an ,为了方便我们加入 a0=−∞ ,很显然序列为空LIS的长度就是0嘛。f[i]代表以第i个数结尾的最长单调子序列的长度。假如LIS加入 ai 之前的最后一个数是 aj ,显然j< i 且 aj<ai ,那么f(i) = f(j) + 1。
那根据这个式子,我们显然应该选择最大的f(j),才能让f(i)最大。于是有了状态转换方程:
f(0)=0,那么对于i>0,总是存在这个j的,因为f(0)=0。
上python代码:
#LIS O(n^2)
import sys
A=[-sys.maxsize]+[3,4,1,2,3,7,6,7]
n=len(A)
f=[0]*(n)
for i in range(1,n):
for j in range(i):
if A[j]1)
#print('j=%d,f[%d]=%d'%(j,i,f[i]))
#前方回溯预警V_V
maxi=f.index(max(f))
res=[A[maxi]]
for i in range(maxi-1,0,-1):
if f[i]==f[maxi]-1:
res.append(A[i])
f[maxi]=f[maxi]-1
print(list(reversed(res)))
如果要求LIS长度的话代码不到10行,后面几行是回溯过程,当年用java写简直累死。
这里置 a0=−∞ 的好处很大,省去判断原序列为空的情景。
以下记录算法过程加深对算法理解:
j=0,f[1]=1
j=0,f[2]=1
j=1,f[2]=2
j=0,f[3]=1
j=1,f[3]=1
j=2,f[3]=1
j=0,f[4]=1
j=1,f[4]=1
j=2,f[4]=1
j=3,f[4]=2
j=0,f[5]=1
j=1,f[5]=1
j=2,f[5]=1
j=3,f[5]=2
j=4,f[5]=3
j=0,f[6]=1
j=1,f[6]=2
j=2,f[6]=3
j=3,f[6]=3
j=4,f[6]=3
j=5,f[6]=4
j=0,f[7]=1
j=1,f[7]=2
j=2,f[7]=3
j=3,f[7]=3
j=4,f[7]=3
j=5,f[7]=4
j=6,f[7]=4
j=0,f[8]=1
j=1,f[8]=2
j=2,f[8]=3
j=3,f[8]=3
j=4,f[8]=3
j=5,f[8]=4
j=6,f[8]=4
j=7,f[8]=5
先从f中找到最大的那个i,其在A中对应的元素就是LIS的最后一个,然后一项一项不断向f[:i]中找最大的即可。
为了方便代码只找一个LIS(所有最大递增子序列中,最大值下标最小,由index函数决定)实际上可以找多个LIS,那就是最大的那个i有多个的情况。
时间复杂度O( n2 ),空间复杂度O( n ),嫌时间复杂度大?事实上这个题有时间复杂度更低的算法。
以{1,6,3,5,4}为例子,我们想像考虑5的时候,之前有两个长度为2的子序列{1,6}和{1,3},那么哪个更“好”呢?显然后者更好,因为3比6小,以3结尾的序列更容易在后面接上一个数。也就是说一个序列,长度为n的递增子序列可能不止一个,但是所有长度为n的子序列中,有一个子序列是比较特殊的,那就是最大元素最小的递增子序列。这就类似贪心策略,那么问题明了了,开始我们只有一个长度为0的单调子序列,末尾大小认为是-∞,这个序列可以用栈来存储(对python来说list就是栈)。对于A里面的元素 ai ,如果比栈顶元素大,那就进栈,如果比他小,那我们就从后往前用它替换第一次比它小的元素(比较拗口,和插入排序插入过程一样,只不过这个是替换),我们替换过去,至少不会变差,这也正式我们保存每个长度“最好”的单调子序列的初衷。那么插入排序有个二分插入法,可以让插入的复杂度变成 logn ,仿照这个做替换,总的时间复杂度也就降低为O( nlogn )
最后LIS的长度就是栈长度。
整个算法过程如图所示,这里省略了最左边的-INF:
那如何找到具体一个子序列呢?
观察图可以发现,
[ 3]
[ 3, 4]
[1, 2, 3]
[ 1, 2, 3, 7]
[1, 2, 3, 6, 7]
均是stack入栈后的序列,只要打印最后一个入栈后的队列即可得到这个序列的LIS,当然打印LIS还有其他办法,大家可以分享。
对应代码:
#LIS O(nlogn)
import sys
A=[3,4,1,2,3,7,6,7]
n=len(A)
stack=[-sys.maxsize]
for i in range(n):
if A[i]>stack[-1]:#如果比栈顶元素大那么加入栈
stack.append(A[i])
res=stack.copy()
else:#二分检索栈中比A[i]大的第一个数
low=0;high=len(stack)-1
while low<=high:
mid=(low+high)//2
if A[i]>stack[mid]:
low=mid+1
else:
high=mid-1
stack[low]=A[i]
print (res[1:])#打印LIS
print(len(stack)-1)#打印LIS长度