LeetCode334递增的三元子序列:LIS问题 & 动态规划 & 贪心 & 二分

前言

  • 大家好,我是新人简书博主:「 个人主页」主要分享程序员生活、编程技术、以及每日的LeetCode刷题记录,欢迎大家关注我,一起学习交流,谢谢!

    • 正在坚持每日更新LeetCode每日一题,发布的题解有些会参考其他大佬的思路(参考资料的链接会放在最下面),欢迎大家关注我 ~ ~ ~
    • 今天是坚持写题解的17天(haha,从21年圣诞节开始的),大家一起加油!
  • 每日一题:LeetCode:334.递增的三元子序列

    • 时间:2022-01-12
    • 力扣难度:Medium
    • 个人难度:Medium
    • 数据结构:数组、LIS最长递增子序列
    • 算法:动态规划、贪心、二分查找

LeetCode每日一题.jpg

2022-01-12:LeetCode:334.递增的三元子序列

1. 题目描述

  • 题目:原题链接

    • 给你一个整数数组 nums ,判断这个数组中是否存在长度为 3 的递增子序列。
    • 如果存在这样的三元组下标 (i, j, k) 且满足 i < j < k,使得 nums[i] < nums[j] < nums[k],返回 true ,否则,返回 false。
  • 输入输出规范

    • 输入:整数数组nums
    • 输出:布尔值,表示是否存在递增三元子序列
  • 输入输出示例

    • 输入:nums = [2,1,5,0,4,6]
    • 输出:true

2. 方法一:动态规划

  • 思路:序列DP + 二分优化复杂度

    • 本题是要判断给定的序列中是否存在长度为3的递增子序列,即是求序列的最长上升子序列(LIS)的长度是否大于3,类似于LC300最长递增子序列

    • 一般而言,LIS问题属于序列DP问题,都可以通过动态规划的思想来解决,其DP三要素也非常明确

      • DP数组:dp[i]表示以序列中的第 i 个元素结尾的最长上升子序列的长度
      • 边界条件:dp[i]初始化为1
      • 状态转移方程:if(nums[j] < nums[i]) dp[i] = Max(dp[i], dp[j] + 1) (j < i)
    • 最长上升子序列的长度求解

      • 两重遍历,外层dp[i]表示以 i 结尾的 LIS 的长度,并将最大值记录到 max 中
      • 内层dp[j]是局部结果,j 的范围是 [0, i),也可以是(i, n),此时状态转移方程需要简单变一下
      public int getLengthOfLIS(int[] nums) { 
          int n = nums.length;
          int max = 1;
          int[] dp = new int[n];
          Arrays.fill(dp, 1);
          for (int i = 0; i < n; i++) {
              for (int j = 0; j < i; j++) {
                  if (nums[j] < nums[i])
                      dp[i] = Math.max(dp[j] + 1, dp[i]);
              }
              max = Math.max(max, dp[i]);
          }
          return max;
      }
      
  • 题解:暴力动态规划,遍历过程中如果长度已经大于3直接返回true

    public boolean increasingTriplet(int[] nums) {
        if(nums == null || nums.length < 3) return false;
        int n = nums.length;
        int[] dp = new int[n];
        Arrays.fill(dp, 1);
        for (int i = 0; i < n; i++) {
            for (int j = i + 1; j < n; j++) {
                if (nums[j] > nums[i])
                    dp[j] = Math.max(dp[i] + 1, dp[j]);
            }
            if(dp[i] >= 3) return true;
        }
        return false;
    }
    
  • 复杂度分析:n 是数组的长度

    • 时间复杂度:O(n^2)
    • 空间复杂度:O(n)

3. 方法二:动态规划 + 二分查找

  • 思路

    • 方法一是LIS问题的常见思路,但是时间复杂度高,会发生TLE超时问题,此时要思考如何进行优化
    • LIS问题的最优解:动态规划 & 二分
      • 首先分析如何确保LIS最长,通过贪心的思想可以发现,当每次向LIS末尾添加的元素是剩余元素中尽可能小的元素时,LIS的增长速率就相对更低
      • 举例说明下,假设LIS的末尾添加了元素A(A > B),那么后续向LIS添加的元素一定大于A,即也会大于B,此时相当于B被遗漏了,LIS不是最长的,所以要添加尽可能小的元素
    • DP数组
      • dp[i]表示指定的序列中,长度为 i 的最长上升子序列的最小结尾元素
      • DP数组是单调上升的,可以通过二分的方式查找该数组中的值
    • 边界条件:dp[0] = nums[0],注意DP数组的个数是 n - 1 还是 n 个
    • 状态转移方程
      • 遍历序列时,如果当前元素大于dp[i],将其添加到DP数组末尾dp[i+1]
      • 如果当前元素小于等于dp[i],通过二分的方式查找当前元素应该放到的DP数组中的位置dp[i - 1] < nums[i] < dp[i],并更新
  • 图解


    LC334递增三元子序列-LIS最长递增子序列问题:动态规划&二分.png
  • 题解:通过二分优化动态规划

    // 方法二:LIS最优解:动态规划(贪心) + 二分
    public boolean increasingTriplet2(int[] nums) {
        if (nums == null || nums.length < 3) return false;
        int n = nums.length;
        int[] dp = new int[n];
        dp[0] = nums[0];
        int len = 1;
        for (int i = 1; i < n; i++) {
            int num = nums[i];
            if (num > dp[len - 1]) {
                dp[len++] = num;
                if (len >= 3) return true;
                continue;
            }
            int left = 0, right = len - 1;
            while (left < right) {
                int mid = (right - left) / 2 + left;
                if (dp[mid] >= num) {
                    right = mid;
                } else {
                    left = mid + 1;
                }
            }
            dp[right] = num;
            if (len >= 3) return true;
        }
        return false;
    }
    
  • 复杂度分析:n 是数组的长度

    • 时间复杂度:O(n*logn),遍历一次O(n),内部嵌套二分查找O(logn),整体O(n*logn)
    • 空间复杂度:O(n)

4. 方法二优化:状态压缩

  • 思路

    • 首先总结下方法二的核心思想
      • 根据贪心的思想,维护一个动态规划DP数组来表示各个长度的LIS的末尾元素
      • 遍历序列的过程中,将大于末尾的元素直接添加,小于的二分查找并更新到DP数组中
    • 虽然这已经是LIS类型题目的最优解法了,但是本题有一个特殊之处,那就是并不是求解LIS最大长度,而是只需要判断该长度是否大于等于3即可
    • 因此,本题无需求解LIS最大长度,也不需要维护长度为 n 的DP数组,而仅需要维护两个变量:最小值、次小值即可
    • 通过这种状态压缩的方式,不仅使得空间优化到O(1)常量空间,同时也无需进行二分查找,只需要判断序列中是否有大于这两个值的元素,时间优化到O(n)
  • 题解:状态压缩优化

    public boolean increasingTriplet(int[] nums) {
        if (nums == null || nums.length < 3) return false;
        int min = Integer.MAX_VALUE;
        int secondMin = Integer.MAX_VALUE;
        for (int num : nums) {
            if (num <= min) min = num;
            else if (num <= secondMin) secondMin = num;
            else return true;
        }
        return false;
    }
    
  • 复杂度分析:n 是数组的长度

    • 时间复杂度:O(n)
    • 空间复杂度:O(1)

5. 方法三:动态规划存储左右侧最小最大值

  • 思路

    • 此外,这里再介绍一种与LIS无关的方法,因为本题要求的是判断是否有递增三元子序列,所以可以使用类似LC42接雨水的解题方法
    • 具体而言,由于要找递增三元组,就可以等价于在序列中寻找一个元素,该元素左侧存在一个小于它的值,右侧存在一个大于它的值
    • 与接雨水一题不同的是,本题需要维护的左侧最小值和右侧最大值是数组,而不像接雨水中是单个数值
    • 因此,维护两个DP数组,分别存放左侧的最小值和右侧的最大值,left[i] 表示序列 [0, i) 中的最小值,right[i] 表示 (i, n)中的最大值
    • 遍历一次序列,计算出两个数组的每一个元素,如果两者满足left[i - 1] < nums[i] < right[i + 1],表示找到了满足的三元组
  • 题解:双向BFS

    // 方法三:另类的动态规划
    public boolean increasingTriplet(int[] nums) {
        if (nums == null || nums.length < 3) return false;
        int n = nums.length;
        // 左侧的最小值数组
        int[] leftMin = new int[n];
        leftMin[0] = nums[0];
        for (int i = 1; i < n; i++) {
            leftMin[i] = Math.min(leftMin[i - 1], nums[i]);
        }
        // 右侧的最大值数组
        int[] rightMax = new int[n];
        rightMax[n - 1] = nums[n - 1];
        for (int i = n - 2; i >= 0; i--) {
            rightMax[i] = Math.max(rightMax[i + 1], nums[i]);
        }
        // 寻找递增三元子序列
        for (int i = 1; i < n - 1; i++) {
            if (leftMin[i - 1] < nums[i] && rightMax[i + 1] > nums[i]) return true;
        }
        return false;
    }
    
  • 复杂度分析:n 是数组的长度

    • 时间复杂度:O(n)
    • 空间复杂度:O(n)

最后

如果本文有所帮助的话,欢迎大家可以给个三连「点赞」&「收藏」&「关注」 ~ ~ ~
也希望大家有空的时候光临我的其他平台,上面会更新Java面经、八股文、刷题记录等等,欢迎大家光临交流,谢谢!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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

推荐阅读更多精彩内容