May 13

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

felix021 @ 2009-5-13 04:15 [IT » 程序设计] 评论(54) , 引用(0) , 阅读(138161) | 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订阅地址: https://www.felix021.com/blog/feed.php
nullptr
2019-10-25 23:37
惊了,09年!我给一个cpp版本的实现,https://gist.github.com/JustZyx/739a1709830f3387ada28b9cb306134e
啦啦啦
2019-10-4 15:46
grin
ponking
2019-9-10 08:55
妙呀
leo
2018-3-3 08:51
http://blog.csdn.net/u013178472/article/details/54926531这个帖子里的第三个和你的一样 但代码不同
leo
2018-3-3 08:40
最后一个7为什么把8换掉了呢
Notzuonotdied Homepage
2018-2-17 10:39
grin默默看了时间——2009-5-13 04:15,看评论都好新。新年快乐,博主。
felix021 回复于 2018-2-27 18:20
新年快乐~
飞花
2017-12-7 10:57
汗,我说错了
飞花
2017-12-6 18:12
亲测,结果不正确#include <stdio.h>#include <malloc.h>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;}int main(){  int L[] = {9,44,32,12,7,8,9,12};  int n = sizeof(L);printf("length:%d",LIS(L,n));  return 0;}竟然输出 11
Tony
2017-10-5 05:03
//在非递减序列 arr[s..e](闭区间)上二分查找第一个大于等于key的位置,如果都小于key,就返回e+1int upper_bound(int arr[], int s, int e, int key)这个方法貌似找到的是第一个大于key的位置吧?不信你自己试试看
yianing
2017-9-6 19:24
我去,说的好清楚,膜拜envy
truddy
2017-7-23 02:24
为什么end数组的长度是n+1?n不够吗
felix021 回复于 2017-7-25 23:13
为了方便, 没有用end[0]
青菜
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
不要求相邻那就这样了
CY
2015-12-22 22:50
在fedora演算了以下却是有问题,刚才举例错了2,5,2,6,8诸如此类.有什么方法能在保持效率的方法下规避调着一个问题呢,请指教
felix021 回复于 2015-12-22 23:48
LIS([2,5,2,8,6], 5) = 3; LIS([2,5,2,6,8], 5) = 4; 有什么问题?
CY
2015-12-22 21:47
比如2,5,2,8,6
CY
2015-12-22 14:06
程序有漏洞,如果子数列的非首项小于前子数列的非首项,会覆盖不完全,从而导致错误。
felix021 回复于 2015-12-22 21:25
比如?
CY
2015-12-22 14:04
1
asdf
2015-11-17 16:17
fear
11 Homepage
2015-11-13 17:24
4,5,1,2,3,7,8,9这样还对么
??
2015-10-31 18:13
shy
weico Email
2015-4-12 10:53
这个算法只能得出LIS的长度???
felix021 回复于 2015-4-14 08:51
能求出长度,求LIS本身就很容易了。
wen
2014-10-16 10:51
看到了Bug版本的二分查找--溢出问题。
felix021 回复于 2014-12-5 15:17
不要在意这些细节uplook
chasuner
2013-4-15 14:00
代码好像有点小问题吧,二分查找的right = len - 1才对吧,然后下面left比较的时候也应该和len - 1 比较!
北叶青藤
2011-11-10 11:40
这是一条隐藏评论或留言。您需要以合适的身份登入后才能看到。
Felix021
2011-11-5 19:50
这是一条隐藏评论或留言。您需要以合适的身份登入后才能看到。
Felix021
2011-11-5 19:50
这是一条隐藏评论或留言。您需要以合适的身份登入后才能看到。
树大
2011-11-5 18:48
这是一条隐藏评论或留言。您需要以合适的身份登入后才能看到。
Felix021
2011-11-4 18:15
这是一条隐藏评论或留言。您需要以合适的身份登入后才能看到。
bigrabbit
2011-3-8 11:26
为什么插入  B[left] = d[i]; 时候,他们不需要比较呢?觉得如果B[left]>d[i],就不需要插入了。。
wwy
2010-7-25 11:18
很好,很强大!very good!
11111
2010-4-30 19:45
envyzan
xg1990
2010-3-1 16:11
学到了~多谢学长~
Iloveyou
2009-8-2 16:47
pigzan
felix021 回复于 2009-8-2 16:49
囧。。这是哪位阿。。。
LN
2009-7-23 09:50
经典
felix021 回复于 2009-7-23 16:19
呵呵。谢谢夸奖。
hot2346 Email
2009-7-7 17:36
楼主你号可爱啊。。。
felix021 回复于 2009-7-7 21:37
@.@ 这个。。跟可爱。。有啥关系。。。
hEAVEN
2009-6-12 15:39
感谢
jyd
2009-6-8 16:46
不错哦,看了就明白算法流程了,谢谢……
felix021 回复于 2009-6-8 18:43
:)
分页: 1/2 第一页 1 2 下页 最后页
发表评论
表情
emotemotemotemotemot
emotemotemotemotemot
emotemotemotemotemot
emotemotemotemotemot
emotemotemotemotemot
打开HTML
打开UBB
打开表情
隐藏
记住我
昵称   密码   *非必须
网址   电邮   [注册]