动态规划系列之最长递增子序列问题解答

今天和大家分享的是动态规划经典问题:最长递增子序列问题解答。(似乎BAT等各大公司都喜欢在面试的时候对动态规划系列经典问题进行笔试。

题目描述:

给定一个整数序列:

求其最长递增子序列(LIS)。如果该序列的一个子序列

其满足且

那么该子序列称为该序列的递增子序列。最长递增子序列就是最长的递增子序列,可能不是唯一的。比如序列为5,2,8,6,3,6,9,7,那么其一个最长递增子序列为2,3,6,9。

思路分析

如果采用暴力穷举,那么时间复杂度是O(2^n),这是难以让人接收的。可以采用动态规划的方式,将问题拆分,我们可以考虑以某一个元素ak为结束元素的最长递增子序列,这里记其长度为L[k],如果我们把所有的情况都计算出来,找到最长的那么就是原问题的最长递增子序列。然后我们要考虑这个子问题是否可以用比其更小的问题递归出来。答案是可以的,如果我们已经知道了L[1],L[2],…,L[k-1],我们可以遍历它们,如果某个元素aj恰好是不大于ak,那么这个递增子序列可以后面加上生成新的递增子序列,并且长度为L[j]+1。我们遍历a1,a2……ak-1,找出最大值就是L[k]。递归公式为:

上面是可以计算出序列的最长递增子序列的长度,时间复杂度为O(n^2)。如果你想得到最终的最长递增子序列,那么可以记录上面递归公式中遍历时的最长情况下的前接元素索引,然后通过这些索引可以重构出最长递增子序列,具体可以参见下面的代码。

具体实现代码(C++)

vector<int> lis(const vector<int>& s)

{
 vector<int> ls(s.size(), 0);
 vector<int> ps(s.size(), -1);
 // 对于k=1
 ls[0] = 1;   // 只有一个元素
 ps[0] = -1;  // 无前接元素
 for (int i = 1; i < s.size(); ++i)
    {
 ls[i] = 1;  // 仅含有自己
 for (int j = 0; j < i; ++j)
        {
 if (s[j] <= s[i] && ls[i] < 1 + ls[j])
            {
 ls[i] = 1 + ls[j];
 ps[i] = j;
            }
        }
    }
 int pos = max_element(ls.begin(), ls.end()) - ls.begin(); // lis的最大元素位置
 vector<int> result;
 result.push_back(s[pos]);
 // 通过前接元素索引重构最长递增子序列
 while (ps[pos] != -1)
    {
 pos = ps[pos];
 result.push_back(s[pos]);
    }
 reverse(result.begin(), result.end());
 return result;
}

优化分析

前面的计算时间复杂度是O(n^2),那么有没有更高效的算法。我们先看看之前算法可以改进的地方,你要找到L[k],你必须要把前面的k-1个子问题遍历一遍,这个是O(n)的时间复杂度。由于前面k-1个问题是无序的,你不知道哪个子问题是最优的,所以我们要全部遍历。如果是有序的话,我们可以采用二分查找,那么时间复杂度将为O(logn)。我们考虑这样的子问题,假定a1,a2……ak-1的LIS为

L,并且我们维护了长度分别为1,2……L的递增子序列的末尾元素的最小值,这里递增子序列长度为i的末尾元素最小值记为M[i]。明显M序列是有序递增的。当加入元素ak时,如果M[l]≥ak,那么序列a1,a2……ak的LIS为L+1,且M[l+1]=ak。否则,你需要遍历M[1],M[2],…,M[l],找到第一个满足M[i]≥ak所对应的i,并且更新M[i]=ak,这时我们更新只是维持数组M的特性,这样后面继续加入新元素,可以重复前面的过程,但是其实元素ak的加入并不会为当前整个序列的LIS做贡献。由于M是有序的,那么可以采用二分查找方式,这样最终的时间复杂度为O(nlogn)。最后M的长度就等于最长递增子序列的长度,但是M并不是最长递增子序列,这点要注意。如果序列是4,1,6,2,8,5,7,3,其整个过程如下图所示:

最终实现代码如下:

vector<int> lis(const vector<int>& s)
{
 vector<int> m;
 // 初始化
 m.push_back(s[0]);
 for (int i = 1; i < s.size(); ++i)
    {
 // lis增加1
 if (s[i] >= m.back())
        {
 m.push_back(s[i]);
        }
 else
        {
 // 利用lower_bound函数找到第一个大于或等于s[i]的位置
            *lower_bound(m.begin(), m.end(), s[i]) = s[i];
        }
    }
 return m;
}

原文发布于微信公众号 - 机器学习算法全栈工程师(Jeemy110)

原文发表时间:2017-09-18

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏深度学习计算机视觉

计算A+B

代码很冗余 时间限制:1秒空间限制:32768K热度指数:347 **算法知识视频讲解 题目描述 给定两个整数A和B,其表示形式是:从个位开始,每三位数用...

2727
来自专栏向治洪

算法笔记之排序

最近在看《算法笔记》,如果单从算法来说,这本书真正做到了短小精悍,首先以排序入题,那么我们今天也来说说排序。 排序 将一堆杂乱无章的元素按照某种规则有序排列的过...

20710
来自专栏强仔仔

Java基础知识-基本数据类型相互转型

这是我第一次系统性的总结java这门语言的基础知识用法,因本人经验有限,所以在总结的过程中如果有错误或者有歧义等等之类的问题,都可以联系我QQ:20801753...

1898
来自专栏韦弦的偶尔分享

Swift 字符串转整数 (atoi) - LeetCode

1、在找到第一个非空字符之前,需要移除掉字符串中的空格字符。如果第一个非空字符是正号或负号,选取该符号,并将其与后面尽可能多的连续的数字组合起来,这部分字符即为...

913
来自专栏Java3y

选择排序就这么简单

选择排序就这么简单 从上一篇已经讲解了冒泡排序了,本章主要讲解的是选择排序,希望大家看完能够理解并手写出选择排序的代码,然后就通过面试了!如果我写得有错误的地方...

38910
来自专栏菩提树下的杨过

数据结构与算法C#版笔记--排序(Sort)-下

5、堆排序(HeapSort) 在接触“堆排序”前,先回顾一下数据结构C#版笔记--树与二叉树 ,其中提到了“完全二叉树”有一些重要的数学特性: ? 上图就是一...

1875
来自专栏逆向技术

计算机基础知识_进制转化

          进制转化 一.任何一个进制转化为10进制的方式 156的十进制可以看做1*10^2 + 5*10^1  +   6*10^0 首先我们看一下...

1960
来自专栏nnngu

算法01 七大排序之:冒泡排序和快速排序

排序是我们生活中经常会面对的问题。同学们做操时会按照从矮到高排列;老师查看上课出勤情况时,会按学生学号顺序点名;高考录取时,会按成绩总分降序依次录取等。排序是数...

3647
来自专栏Jack-Cui

150.Evaluate Reverse Polish Notation(Stack-Medium)

Evaluate the value of an arithmetic expression in Reverse Polish Notation. Valid...

18610
来自专栏令仔很忙

新手学JAVA(六)----处理随机性的数据

在我们的日常生活中会遇到很多随机性的事情,比如:摇奖,彩票,掷色子,这些都可以通过程序计算其中奖的概率。在JAVA的类库中,有一个专门操作这种随机性数据的类—-...

712

扫码关注云+社区