lrj 9.4.1 最长上升子序列 LIS

p275

d(i)是以Ai为结尾的最长上升子序列的长度

 

《算法竞赛入门经典-训练指南》p62 问题6 提供了一种优化到 O(nlogn)的方法。

 文本中用g(i)表示d值为i的最小状态编号(数组下标),满足

 g(1) <= g(2) <= g(3) <= ... <= g(n)

 可以用反证法:

 假设 i < j, g(i) > g(j)

 g(j)代表 d(x) = j 的最小 x,对应的LIS的长度为j,最后一个元素是Ax,设这个LIS为LISj

 g(i)代表 d(y) = i 的最小 y,对应的LIS的长度为i,最后一个元素是Ay

 LISj有j个成员,其中取i个成员(不包括最后一个元素)也是一个LIS,长度为i,这个LIS的最后一个成员的下标 < g(j) < g(i),而 g(i) 应该是长度为i的LIS的最小下标,所以矛盾,得证 #

 

 代码中实际上是用g(i)表示d值为i的多个LIS中,最后一个元素最小的LIS的最后一个元素的值,也满足

 g(1) <= g(2) <= g(3) <= ... <= g(n)

 可以用反证法:

 假设 i < j, g(i) > g(j)

 g(j)代表 d(x) = j 的最小 x,对应的LIS的长度为j,最后一个元素是x,设这个LIS为LISj

 g(i)代表 d(y) = i 的最小 y,对应的LIS的长度为i,最后一个元素是y

 LISj有j个成员,其中取i个成员(不包括最后一个元素)也是一个LIS,长度为i,这个LIS的最后一个成员的值 = z < g(j) = x < g(i) = y,而 g(i) 应该是长度为i的LIS的末尾元素最小值,所以矛盾,得证 #

 

    for (int i = 1; i <= n; i++) g[i] = INF;

    for (int i = 0; i < n; i++) {
        // k是满足 g[k] >= A[i] 的最小下标
        // k' = k - 1 是满足 g[k] < A[i] 的最大(最后一个)下标,A[i] 可以放在g[k-1]对应的长度为k-1的LIS的后面,形成一个新的长度为k的LIS
        // 这个新的LIS的长度为k,最后一个元素是A[i], 所以设置 d[i] = k; 
        // 此时g[k] >= A[i], 所以设置 g[k] = A[i];
        int k = lower_bound(g + 1, g + n + 1, A[i]) - g; // 在g[1]到g[n]中找
        d[i] = k;
        g[k] = A[i];
    }

数组g[]是排序的

最后的答案是 lower_bound(g + 1, g + 1+ n, INF),也就是数组g[]中不是INF的最大下标

 

《挑战程序设计竞赛》p64 也有类似分析

 

 

 网上有个例子

假设存在一个序列A[1..9] = 2 1 5 3 6 4 8 9 7,可以看出来它的LIS长度为5。n 下面一步一步试着找出它。 我们定义一个序列g,然后令 i = 1 to 9 逐个考察这个序列。 此外,我们用一个变量Len来记录现在最长算到多少了

首先,把A[1]有序地放到g里,令g[1] = 2,就是说当只有1个数字2的时候,长度为1的LIS的最小末尾是2。这时Len=1

然后,把A[2]有序地放到g里,令g[1] = 1,就是说长度为1的LIS的最小末尾是1,A[1]=2已经没用了,很容易理解吧。这时Len=1

接着,A[3] = 5,A[3]>g[1],所以令g[1+1]=g[2]=A[3]=5,就是说长度为2的LIS的最小末尾是5,很容易理解吧。这时候g[1..2] = 1, 5,Len=2

再来,A[4] = 3,它正好加在1,5之间,放在1的位置显然不合适,因为1小于3,长度为1的LIS最小末尾应该是1,这样很容易推知,长度为2的LIS最小末尾是3,于是可以把5淘汰掉,这时候g[1..2] = 1, 3,Len = 2

继续,A[5] = 6,它在3后面,因为g[2] = 3, 而6在3后面,于是很容易可以推知g[3] = 6, 这时g[1..3] = 1, 3, 6,还是很容易理解吧? Len = 3 了噢。

第6个, A[6] = 4,你看它在3和6之间,于是我们就可以把6替换掉,得到g[3] = 4。g[1..3] = 1, 3, 4, Len继续等于3

第7个, A[7] = 8,它很大,比4大,嗯。于是g[4] = 8。Len变成4了

第8个, A[8] = 9,得到g[5] = 9,嗯。Len继续增大,到5了。

最后一个, A[9] = 7,它在g[3] = 4和g[4] = 8之间,所以我们知道,最新的g[4] =7,g[1..5] = 1, 3, 4, 7, 9,Len = 5。

于是我们知道了LIS的长度为5。

!!!!! 注意。这个1,3,4,7,9不是LIS,它只是存储的对应长度LIS的最小末尾。有了这个末尾,我们就可以一个一个地插入数据。

虽然最后一个A[9] = 7更新进去对于这组数据没有什么意义,但是如果后面再出现两个数字 8 和 9,那么就可以把8更新到A[5], 9更新到A[6],得出LIS的长度为6。

然后应该发现一件事情了:在g中插入数据是有序的,而且是进行替换而不需要挪动——也就是说,我们可以使用二分查找,将每一个数字的插入时间优化到O(logN)~~~~~于是算法的时间复杂度就降低到了O(NlogN)~!

 

另一个网上的说明

对于序列Sn,考虑其长度为i的单调子列(1<=i<=m),这样的子列可能有多个。我们选取这些子列的结尾元素(子列的最后一个元素)的最小值。用Li表示。易知

L1<=L2<=…<=Lm

如果Li>Lj(i<j),那么去掉以Lj结尾的递增子序列的最后j-i个元素,得到一个长度为i的子序列,该序列的结尾元素ak<=Lj<Li,这与Li标识了长度为i的递增子序列的最小结尾元素相矛盾,于是证明了上述结论。

 

现在,我们来寻找Sn对应的L序列,如果我们找到的最大的Li是Lm,那么m就是最大单调子列的长度。下面的方法可以用来维护L。

 

从左至右扫描Sn,对于每一个ai,它可能

(1)    ai<L1,那么L1=ai

(2)    ai>=Lm,那么Lm+1=ai,m=m+1 (其中m是当前见到的最大的L下标)

(3)    Ls<=ai<Ls+1,那么Ls+1=ai

 

扫描完成后,我们也就得到了最长递增子序列的长度。从上述方法可知,对于每一个元素,我们需要对L进行查找操作,由于L有序,所以这个操作为logn,于是总的复杂度为O(nlogn)。

 

posted @ 2016-08-22 17:48  PatrickZhou  阅读(166)  评论(0编辑  收藏  举报