leetcode实战——300.最长上升子序列(动态规划+分治法)

300. 最长上升子序列

题目

给定一个无序的整数数组,找到其中最长上升子序列的长度。

示例:

输入: [10,9,2,5,3,7,101,18]
输出: 4
解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。

说明:

  • 可能会有多种最长上升子序列的组合,你只需要输出对应的长度即可。
  • 你算法的时间复杂度应该为 O(n^2)

思路

首先看到这道题,刷题比较少的同学可能上来就是两眼一抹黑,除了用暴力解法完全没有思路。不过我可以告诉你遇到这种数组类型的中等难度的题目,并且要求得到的结果只是一个数字的题目,基本上思路都可以往动态规划方面去想,最典型的例子就是最大子序列和问题,有没有感觉很类似!

首先是需要介绍以下子串子序列的区别:

  • 子串:substring,是原字符串里面的连续的字符串,注意是连续的
  • 子序列:subsequence,是由原字符串中的字符组成的一个序列,并不一定是连续的,但是相对的顺序是需要保证的

比如helloworld里面,ello或者oworl是一个子串,而hld这种就是一个子序列,子序列不要求在原字符串是连起来的。

这道题还特别细心的提醒了你时间复杂度应该是O(n^2)及以下的,所以基本排除了暴力法,所以这个时候我们想想应该怎么用动态规划的方法解决问题。

解法一 动态规划

OK,现在我们的解题的方向已经有了,就像是高考数学题已经知道应该用什么公式就差找到公式的用法了。我们知道应用动态规划必须要使问题具有最优子问题子问题重叠性,只有满足这两个条件才能用动态规划来解。我们先来定义我们要用到的变量,也是动态规划题目里面的一个套路,就是定义状态

max[i]

表示从第1个元素到第i+1个元素范围内包含第i+1个元素的最长上升子序列的长度。

好了我相信很多想到动态规划的人已经料到了我们会定义这个状态,但是这个公式接下去怎么发展呢?怎么用这个状态组成我们的状态转移方程呢?max[i]max[<i]的元素是什么关系?

回顾一下题目发现,这个子序列必须是上升的,所以在计算max[i]的时候,对于两个位置ij,如果j < i,我们有两种情况:

  • 如果num[j] >= num[i],那么这两个元素肯定不能存在于同一个子序列里面的,所以直接跳过不用任何操作
  • 如果num[j] < num[i],这两个元素可以存在于同一个子序列里面,于是可能存在max[j] + 1 = max[i]

对于第二个结论,我们并不确定等式是否成立,因为这个时候只考虑了原来以位置j结尾的子序列和i位置元素构成的子序列,其他的以i结尾的子序列有可能比这个大,我们并不确定,所以需要遍历所有的满足j < ij的值,所以这个状态转移公式可以这样表示:

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;
    }

这个答案里面有一个两层的循环,所以时间复杂度是O(n^2)。只用了一个数组,所以空间复杂度是O(n)。遗憾的是在提交了答案之后,时间只击败了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] = 3
k = 1 : [4, 5], [5, 6]       => tail[1] = 5
k = 2 : [4, 5, 6]            => tail[2] = 6

观察一下这个tail数组发现这个数组是按照升序排列的,为了证明所有的情况下tail数组都是升序排列,我们用反证法证明一下这个数组的确是升序排列的。

假设存在这样的一组a < btail[a] >= tail[b],那么在以tail[b]结尾的长度为b+1的子序列中必然存在一个长度为a+1的子序列,并且这个子子序列的最后一个元素小于tail[b]也必然小于tail[a](因为这个序列也是递增的),这样就出现了长度为a+1的子序列的最后元素最小值小于tail[a]这样的矛盾,所以这样的假设就是不成立的。

在遍历的过程中,假设当前的发现的最大长度是k+1,我们有元素nums[i]:

  1. 如果nums[i]tail数组里面所有的元素都大,也就是比长度为k+1的子序列的最后一个元素还要大,所以把这个元素加上去就可以得到一个长度为k+2的子序列,这个时候我们创建了一个新的值tail[k+1] = nums[i],并且有tail[k+1] > 任何tail[<k+1],也就是说tail[k+1]是数组里面最大的。每次添加新元素比当前数组里面所有的元素大,所以添加操作会保持数组的升序。

  2. 如果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数组位置的过程中,我们采用二分查找的方法,可以大大提高效率,把更新操作的时间复杂度提高到O(logn)水平。一次遍历以及每次遍历的二分查找,得到最后的时间复杂度就是O(nlogn)

用一个简单的例子来模拟一下整个的更新的过程:

数组[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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,529评论 5 475
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,015评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,409评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,385评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,387评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,466评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,880评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,528评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,727评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,528评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,602评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,302评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,873评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,890评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,132评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,777评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,310评论 2 342

推荐阅读更多精彩内容