最长递增子序列

限于喜欢 提交于 2020-01-25 05:16:38

《编程之美》2.16节

 这是一个DP(动态规划)问题。
    以串1, -1, 2, -3, 4, -5, 6, -7为例,递归子结构为:
    incr[i] = max{1, LIS[k]+1};对k <i, 且array[k] < array[i]
 LIS表示以array[i]为最大元素的最长递增子序列的长度.prev[N]用于存储该元素的前驱元素。
 1 def findInc(array):
 2     incr = [1] * len(array)
 3     prev = [-1] * len(array)    #record the sequence's previous element
 4 
 5     for i in range(len(array)):
 6         for j in range(0, i):
 7             if array[j] < array[i] and incr[i] < incr[j]+1:
 8                 incr[i] = incr[j] + 1
 9                 prev[i] = j
10     
11     print "The length is " + str(max(incr))
12     print "The subsequence is "
13     inx = incr.index(max(incr))
14     result = []
15     while prev[inx] != -1:
16         result.append(array[inx])
17         inx = prev[inx]
18     result.append(array[inx])
19     result.reverse()
20     print result
21 
22 array1 = [1, -3, 2, -1, 4, -5, 6, 0];
23 findInc(array1)

输出如下:
The length is 4
The subsequence is
[1, 2, 4, 6]


上面的是基本方法,需要O(N^2)的时间复杂度。根据编程之美的解答,如果我们记录每个递增子序列长度就能够利用这些额外信息进行优化,以下转自:http://blog.csdn.net/beiyeqingteng/article/details/6954571

假设存在一个序列d[1..9] = 2 1 5 3 6 4 8 9 7,可以看出来它的LIS长度为5。
下面一步一步试着找出它。
我们定义一个序列B,然后令 i = 1 to 9 逐个考察这个序列。
此外,我们用一个变量Len来记录现在最长算到多少了
首先,把d[1]有序地放到B里,令B[1] = 2,就是说当只有1一个数字2的时候,长度为1的LIS的最小末尾是2。这时Len=1
然后,把d[2]有序地放到B里,令B[1] = 1,就是说长度为1的LIS的最小末尾是1,d[1]=2已经没用了,很容易理解吧。这时Len=1
接着,d[3] = 5,d[3]>B[1],所以令B[1+1]=B[2]=d[3]=5,就是说长度为2的LIS的最小末尾是5,很容易理解吧。这时候B[1..2] = 1, 5,Len=2
再来,d[4] = 3,它正好加在1,5之间,放在1的位置显然不合适,因为1小于3,长度为1的LIS最小末尾应该是1,这样很容易推知,长度为2的LIS最小末尾是3,于是可以把5淘汰掉,这时候B[1..2] = 1, 3,Len = 2
继续,d[5] = 6,它在3后面,因为B[2] = 3, 而6在3后面,于是很容易可以推知B[3] = 6, 这时B[1..3] = 1, 3, 6,还是很容易理解吧? Len = 3 了噢。
第6个, d[6] = 4,你看它在3和6之间,于是我们就可以把6替换掉,得到B[3] = 4。B[1..3] = 1, 3, 4, Len继续等于3
第7个, d[7] = 8,它很大,比4大,嗯。于是B[4] = 8。Len变成4了
第8个, d[8] = 9,得到B[5] = 9,嗯。Len继续增大,到5了。
最后一个, d[9] = 7,它在B[3] = 4和B[4] = 8之间,所以我们知道,最新的B[4] =7,B[1..5] = 1, 3, 4, 7, 9,Len = 5。
于是我们知道了LIS的长度为5。
!!!!! 注意。这个1,3,4,7,9不是LIS,它只是存储的对应长度LIS的最小末尾。有了这个末尾,我们就可以一个一个地插入数据。虽然最后一个d[9] = 7更新进去对于这组数据没有什么意义,但是如果后面再出现两个数字 8 和 9,那么就可以把8更新到d[5], 9更新到d[6],得出LIS的长度为6。
然后应该发现一件事情了:在B中插入数据是有序的,而且是进行替换而不需要挪动——也就是说,我们可以使用二分查找,将每一个数字的插入时间优化到O(logN)~~~~~于是算法的时间复杂度就降低到了O(NlogN)~!

 1 def find(array):
 2   incr = [1] = len(array) 3     B = [0] * len(array)
 4     B[0] = array[0]
 5 
 6     maxlen = 1               #current length of B
 7     for i in range(1, len(array)):
 8         left = 0
 9         right = maxlen-1
10         while left <= right:
11             mid = left + (right-left)/2
12             if(B[mid] < array[i]):
13                 left = mid+1
14             else:
15                 right = mid-1
16         B[left] = array[i]           #这句可能是对0~maxlen-1中的替换,也可能是B[maxlen]的赋值    
17         if left >= maxlen:            #说明在上面的二分查找中left被移动到了数组末尾,就是说此时递增子序列增长了
18             maxlen += 1
19     return maxlen
20 
21 
22 array = [1, -3, 2, -1, 4, -5, 6, 0]
23 print find(array)

上面代码的关键在与理解10~15行的二分查找:如果在一个数组里面查找一个数,而这个数在数组里不存在,那么通过二分查找时,退出while循环的时候,如果那个数比数组里最大的还大,那么,left会是数组长度+1,而如果这个数的大小在数组范围内,但该值并不存在于数组,那么,最后left会停在第一个比那个值大的位置。

更进一步分析这种方法:
上面的程序输出会得到4,也就是只得到了LIS的最大值,如果我们要得到LIS数组,也就是程序1中的incr数组。就是说要统计array序列中每个以array[i]结尾的最长递增子序列的长度,实际上,在二分查找过程中,left的值就表示的这一层含义。所以只要在16行后添加 incr[i] = left+1,即可得到incr。

另外能不能打印出找到的递增子序列,方法没有想到,碰到的问题是,因为我们B数组存储的总是相应元素的最小记录,在查找过程中,不论是用prev[]前驱方法还是result[]直接记录,子序列的内容总在被不断破坏(但是个数是不变的)。看过下面的代码就清楚了:

 1 def find(array):
 2     incr = [1] * len(array)
 3     result = []            #used to store the subsequence
 4     B = [0] * len(array)
 5     B[0] = array[0]
 6 
 7     maxlen = 1               #current length of B
 8     result.append(array[0])
 9     for i in range(1, len(array)):
10         left = 0
11         right = maxlen-1
12         while left <= right:
13             mid = left + (right-left)/2
14             if(B[mid] < array[i]):
15                 left = mid+1
16             else:
17                 right = mid-1
18 
19         if left >= maxlen:
20             maxlen += 1
21             result.append(array[i])
22         else:
23             result[left] = array[i]
24         B[left] = array[i] 
25         
26         # the position of left indicate how many numbers less than array[i], so
27         # it can be used to update incr[],
28         incr[i] = left+1
29     print incr           #print the LIS array
30     print result
31     return maxlen
32 
33 
34 array = [1, -3, 2, -1, 4, -5, 6, 0]
35 print find(array)

上面代码的输出是:
[1, 1, 2, 2, 3, 1, 4, 3]
[-5, -1, 0, 6]  #result数组
4

result数组并不是我们想象-3, -1, 4, 6. 问题在23行,

对array数组,在从1到-3是,我们希望result[0]能从1更新为-3,所以23行需要执行,但是到array中的-5时,我们又希望不更新result,23行不应该被执行。所以这条路走不通。


最后一个扩展问题:从一列数中筛除尽可能少的数使得从左往右看,这些数是从小到大再从大到小的。

假设是一个数组arr[n], 它的分段点是 i (0-i 递增, i 到 n-1 递减), 假设我们用方法LIS(i) 找到最长的从0到 i 的递增子序列,LDS(i) 找到从 i 到 n -1的最长递减子序列,那么它的总长度为 LIS(i) + LDS(i) -1, 所以我们扫描整个数组,即让 i 从0 到 n-1, 找出使 LIS(i) + LDS(i) -1 最大的即可。代码如下:

 1 array = [1, 7, 4, 5, 9, 8, 2, 6, 3, 0]
 2 
 3 b = [1] * len(array)
 4 prevb = [-1] * len(array)
 5 
 6 for i in range(1, len(array)):       #from the second element start
 7     for j in range(0, i):
 8         if array[j] < array[i] and b[i] < b[j] +1:
 9             b[i] = b[j] + 1
10             prevb[i] = j
11 print b
12 
13 c = [1] * len(array)
14 prevc = [-1] * len(array)
15 for i in range(len(array)-2, -1, -1):
16     for j in range(len(array)-1, i, -1):
17         if array[j] < array[i]  and c[i] < c[j] + 1:
18             c[i] = c[j] + 1
19             prevc[i] = j
20 print c
21 
22 maxv = 0
23 point = 0
24 for i in range(len(array)):
25     if b[i] + c[i] > maxv:
26         maxv = b[i] + c[i]
27         point = i
28 print point
29 print "at least remove number: " + str(len(array) + 1 - maxv)
30 print "the left sequence is: "
31 result = []
32 ppoint =  point
33 
34 while(prevb[point] != -1):
35     result.append(array[point])
36     point = prevb[point]
37 result.append(array[point])
38 result.reverse()
39 result.remove(array[ppoint])    #this number repeat in b[i] and c[i]
40 
41 while prevc[ppoint] != -1:
42     result.append(array[ppoint])
43     ppoint = prevc[ppoint]
44 result.append(array[ppoint])
45 print result

程序输出如下:

[1, 2, 2, 3, 4, 4, 2, 4, 3, 1]
[2, 4, 3, 3, 5, 4, 2, 3, 2, 1]
4
at least remove number: 2
the left sequence is:
[1, 4, 5, 9, 8, 6, 3, 0]

 

 

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!