May 13

最长递增子序列 O(NlogN)算法 不指定

felix021 @ 2009-5-13 04:15 [IT » 程序设计] 评论(43) , 引用(0) , 阅读(64877) | Via 本站原创 | |
今天回顾WOJ1398,发现了这个当时没有理解透彻的算法。
看了好久好久,现在终于想明白了。
试着把它写下来,让自己更明白。

最长递增子序列,Longest Increasing Subsequence 下面我们简记为 LIS。
排序+LCS算法 以及 DP算法就忽略了,这两个太容易理解了。

假设存在一个序列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)~!

代码如下:

//在非递减序列 arr[s..e](闭区间)上二分查找第一个大于等于key的位置,如果都小于key,就返回e+1
int upper_bound(int arr[], int s, int e, int key)
{
    int mid;
    if (arr[e] <= key)
        return e + 1;
    while (s < e)
    {
        mid = s + (e - s) / 2;
        if (arr[mid] <= key)
            s = mid + 1;
        else
            e = mid;
    }
    return s;
}

int LIS(int d[], int n)
{
    int i = 0, len = 1, *end = (int *)alloca(sizeof(int) * (n + 1));
    end[1] = d[0]; //初始化:长度为1的LIS末尾为d[0]
    for (i = 1; i < n; i++)
    {
        int pos = upper_bound(end, 1, len, d[i]); //找到插入位置
        end[pos] = d[i];
        if (len < pos) //按需要更新LIS长度
            len = pos;
    }
    return len;
}


update @ 2016-08-21

没想到7年多了还要更新一下……

有几位同学在评论中问到如何给出一个LIS而不仅是计算长度。具体的代码我没有写过,不过大概可以这么实现:更新B[i]的时候,把记下来数字在原来数组中的下标也记下来(被替换的数据保留在一个后备数组中)。等到得出 B[n] 了以后,用贪心算法往前回溯,每次找出B[i-1]对应后备数组中值小于B[i]、下标小于B[i]下标、且在该后备数组中下标最大的那个。

update @ 2017-04-16

补充一下,由于上面那段代码用的是upper_bound,所以实际上求的是最长不下降子序列;如果要求递增子序列,应该改用lower_bound。

转载请注明出自 ,如是转载文则注明原出处,谢谢:)
RSS订阅地址: http://www.felix021.com/blog/feed.php
青菜
2017-7-14 20:07
‘B[i-1]对应后备数组中’ 意思是每个B[i]都有一个不同的后备数组吗
felix021 回复于 2017-7-15 16:21
嗯,这个是我的粗略想法,没有实现过,也许有更好的方法:)
Eric
2017-7-2 11:37
。。。666看到上古博客下意识看了一下时间。。。。2!0!1!7!年!。。。。
anoymous
2017-4-16 14:35
写的非常清楚!果然大牛讲的不一样。
111111
2017-4-10 22:49
感觉是上古博客啊。哈哈
felix021 回复于 2017-4-23 00:36
因为页面太丑了是吗....
sway
2016-11-24 02:14
我擦,咋粘码。。
felix021 回复于 2016-11-26 10:36
这个blog系统太老了,我懒得升级,不过你可以粘gist地址
sway
2016-11-24 02:12
。。。
披萨大叔
2016-8-20 15:04
辅助数组是1,3,4,7,9,LIS是1,3,4,8,9可否举个例子,如何根据辅助数组,还原LIS。
nima
2016-8-4 10:40
2,6,7,3,4,5,6,7还对不对
azor
2016-7-10 16:27
{ 8, 6, 15, 14, 20, 21 }输出为5,但给出的思想绝对甚好!
felix021 回复于 2016-7-11 10:11
你确定用的是我的代码?我跑出来结果是4,似乎没问题。
半壶酒
2016-7-4 16:26
请教这个算法怎么还原子序列呢?
felix021 回复于 2016-7-5 13:22
更新B[i]的时候,把记下来数字在原来数组中的下标也记下来(被替换的数据保留在一个后备数组中)。等到得出 B[n] 了以后,用贪心算法往前回溯,每次找出B[i-1]对应后备数组中值小于B[i]、下标小于B[i]下标、且在该后备数组中下标最大的那个。
学习算法
2016-6-15 21:59
别的教程一堆字根本看不懂,就你的这个写的明白。谢谢!
htxy
2016-4-1 11:16
此法甚屌
Mort
2015-12-31 11:24
写编程的文章第一个序数不是0看着真别扭
felix021 回复于 2015-12-31 13:59
阁下大概是没用过basic和lua吧XD
CY
2015-12-23 08:40
不要求相邻这么做也太low了
felix021 回复于 2015-12-23 19:38
要求相邻的话O(n)就行了。不要求相邻的话,如果阁下有不low的做法还请赐教。
CY
2015-12-23 08:38
不要求相邻那就这样了
分页: 1/3 第一页 1 2 3 下页 最后页
发表评论
表情
emotemotemotemotemot
emotemotemotemotemot
emotemotemotemotemot
emotemotemotemotemot
emotemotemotemotemot
打开HTML
打开UBB
打开表情
隐藏
记住我
昵称   密码   *非必须
网址   电邮   [注册]