leetcode实战——300.最长上升子序列(动态规划+分治法)
300. 最长上升子序列
题目
给定一个无序的整数数组,找到其中最长上升子序列的长度。
示例:
输入: [10,9,2,5,3,7,101,18]
输出: 4
解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。
说明:
- 可能会有多种最长上升子序列的组合,你只要要输出对应的长度就可。
- 你算法的时间复杂度应该为
。
思路
首先看到这道题,刷题比较少的同学可能上来就是两眼一抹黑,除了用暴力解法完全没有思路。不过我可以告诉你遇到这种数组类型的中等难度的题目,并且要求得到的结果只是一个数字的题目,基本上思路都可以往动态规划方面去想,最典型的例子就是最大子序列和问题,有没有感觉很相似!
首先是需要详情以下子串和子序列的区别:
- 子串:substring,是原字符串里面的连续的字符串,注意是连续的
- 子序列:subsequence,是由原字符串中的字符组成的一个序列,并不肯定是连续的,但是相对的顺序是需要保证的
比方helloworld里面,ello或者者oworl是一个子串,而hld这种就是一个子序列,子序列不要求在原字符串是连起来的。
这道题还特别细心的提示了你时间复杂度应该是及以下的,所以基本排除了暴力法,所以这个时候我们想想应该怎样用动态规划的方法处理问题。
解法一 动态规划
OK,现在我们的解题的方向已经有了,就像是高考数学题已经知道应该用什么公式就差找到公式的用法了。我们知道应用动态规划必需要使问题具备最优子问题和子问题重叠性,只有满足这两个条件才能用动态规划来解。我们先来定义我们要用到的变量,也是动态规划题目里面的一个套路,就是定义状态:
max[i]
表示从第1个元素到第i+1个元素范围内包含第i+1个元素的最长上升子序列的长度。
好了我相信很多想到动态规划的人已经料到了我们会定义这个状态,但是这个公式接下去怎样发展呢?怎样用这个状态组成我们的状态转移方程呢?max[i]和max[<i]的元素是什么关系?
回顾一下题目发现,这个子序列必需是上升的,所以在计算max[i]的时候,对于两个位置i和j,假如j < i,我们有两种情况:
- 假如
num[j] >= num[i],那么这两个元素一定不能存在于同一个子序列里面的,所以直接跳过不用任何操作 - 假如
num[j] < num[i],这两个元素可以存在于同一个子序列里面,于是可能存在max[j] + 1 = max[i]
对于第二个结论,我们并不确定等式能否成立,由于这个时候只考虑了原来以位置j结尾的子序列和i位置元素构成的子序列,其余的以i结尾的子序列有可能比这个大,我们并不确定,所以需要遍历所有的满足j < i的j的值,所以这个状态转移公式可以这样表示:
max[i] = max( max[0], max[1] ... max[i-1] ) + 1
这样得到的值确保是i之前的最长序列和当前元素组成子序列,也就是以nums[i]结尾的最长序列。
代码如下:
public int lengthOfLIS(int[] nums) { if (nums == null || nums.length < 1) { return 0; } else if (nums.length == 1) { return 1; } int[] max = new int[nums.length]; int globalMax = 1; // 保存到当前位置为止最大长度 Arrays.fill(max, 1); // 初始化的最大值就是单个元素的长度1 for (int i = 0; i < nums.length; i++) { // 外层循环遍历从 0 到 num.length for (int j = 0; j < i; j++) { // 内层循环遍历从 0 到 i if (nums[i] > nums[j]) { // 只有小于nums[i]的元素才考虑进来 max[i] = Math.max(max[i], max[j] + 1); } } globalMax = Math.max(globalMax, max[i]); //升级最大长度 } return globalMax; }这个答案里面有一个两层的循环,所以时间复杂度是。只用了一个数组,所以空间复杂度是
。遗憾的是在提交了答案之后,时间只击败了31%的客户,相比来说不是特别理想,还有没有其余的解法呢。这个时候我们可以把目光投向最经典的分治法。
解法二 分治法+动态规划
说实话这个方法一般人是真心想不到,更别说在在面试的时候能够在30分钟内想出这个处理方案。在网上已经有很多人讲过了这个方法,特别是动态规划设计方法&&纸牌游戏讲解二分解法这篇文章用一个叫patience game的纸牌游戏来模拟了整个计算的过程,非常浅显易懂,只需按照它的纸牌分发的逻辑来写代码即可以得到最后的结果,遗憾的时候这篇文章并没有写证实的过程。在接下来我会尽量讲解清楚整个推导模拟过程。
首先我们要先定义一个状态也就是一个数组:
tails[k]
代表所有长度为k+1的最小子序列的最后一个元素的最小值,这里有三个限制条件,1.长度为k+1的子序列,2.最后一个元素,3.最小值。比方数组[4,5,6,3],所对应的状态是:
k = 0 : [4], [5], [6], [3] => tail[0] = 3k = 1 : [4, 5], [5, 6] => tail[1] = 5k = 2 : [4, 5, 6] => tail[2] = 6观察一下这个tail数组发现这个数组是按照升序排列的,为了证实所有的情况下tail数组都是升序排列,我们用反证法证实一下这个数组确实是升序排列的。
假设存在这样的一组a < b有tail[a] >= tail[b],那么在以tail[b]结尾的长度为b+1的子序列中必然存在一个长度为a+1的子序列,并且这个子子序列的最后一个元素小于tail[b]也必然小于tail[a](由于这个序列也是递增的),这样就出现了长度为a+1的子序列的最后元素最小值小于tail[a]这样的矛盾,所以这样的假设就是不成立的。
在遍历的过程中,假设当前的发现的最大长度是k+1,我们有元素nums[i]:
假如
nums[i]比tail数组里面所有的元素都大,也就是比长度为k+1的子序列的最后一个元素还要大,所以把这个元素加上去即可以得到一个长度为k+2的子序列,这个时候我们创立了一个新的值tail[k+1] = nums[i],并且有tail[k+1] > 任何tail[<k+1],也就是说tail[k+1]是数组里面最大的。每次增加新元素比当前数组里面所有的元素大,所以增加操作会保持数组的升序。假如
nums[i]不是最大的,我们就需要升级这个数组,找到j使得tail[j-1] < num[i] <= tail[j],我们把这个tail[j]的值设置成num[i]。在这一步中,我们发现了num[i]比长度为j的数组的最后一个元素要大,所以可以增加nums[i]到长度为j的子序列的最后变成了长度为j+1的子序列,然而这个nums[i]比长度为j+1子序列的最后一个元素的最小值也就是tail[j]还要小,所以就升级tail[j]为nums[i]。这个操作很显著不会改变这个tail序列的升序性。
经过遍历之后,得到最终的tail的数组,这个数组的最后的长度就是我们需要的最长子序列的长度了。
在上面的寻觅nums[i]在tail数组位置的过程中,我们采用二分查找的方法,可以大大提高效率,把升级操作的时间复杂度提高到水平。一次遍历以及每次遍历的二分查找,得到最后的时间复杂度就是
。
用一个简单的例子来模拟一下整个的升级的过程:
数组[10,9,2,5,3,7,101,18],从左往右进行遍历,每一步都是经过上面的步骤进行计算:
| nums | 10 | 9 | 2 | 5 | 3 | 7 | 101 | 18 |
|---|---|---|---|---|---|---|---|---|
| tail[0] | 10 | 9 | 2 | 2 | 2 | 2 | 2 | 2 |
| tail[1] | 5 | 3 | 3 | 3 | 3 | |||
| tail[2] | 7 | 7 | 7 | |||||
| tail[3] | 101 | 18 |
通过上面的讲解,整个解题的思路已经基本清楚了,不过在这道题里面还有一个隐藏的考点就是二分查找法查找右侧边界,这个也可以单开一篇文章,这里就不多说了,可以参考这篇题解
代码如下:
public int lengthOfLIS(int[] nums) { int[] tail = new int[nums.length]; int size = 0; // 数组大小计数 for (int x : nums) { int i = 0, j = size; while (i != j) { // 二分查找右边界 int m = (i + j) / 2; if (tail[m] < x) i = m + 1; else j = m; } tail[i] = x; if (i == size) ++size; } return size;}总结
其实对于大多数人只需知道了第一种解法就基本掌握了动态规划的思路,核心要点就是找到正确的状态转移方程,而后进行遍历。第二种解法可以作为拓展思维,供大家学习娱乐参考。
更多内容请看我的个人博客
参考
动态规划 、贪心算法 + 二分
动态规划设计方法&&纸牌游戏讲解二分解法
[Lintcode] Longest Increasing Subsequence 最长上升序列
Java/Python Binary search O(nlogn) time with explanation
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » leetcode实战——300.最长上升子序列(动态规划+分治法)