leetCode 300.最长递增子序列 (贪心 + 二分 ) + 图解 + 优化 + 拓展

news2025/1/11 20:01:37

300. 最长递增子序列 - 力扣(LeetCode)

给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。

子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。

示例 1:

输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。

示例 2:

输入:nums = [0,1,0,3,2,3]
输出:4

示例 3:

输入:nums = [7,7,7,7,7,7,7]
输出:1

>>分析:

  • 递增子序列           IS,Increasing Subsequence
  • 最长递增子序列    LIS,Longest Increasing Subsequence
  • O(n^2)           回溯  -->  记忆化搜索  -->  递推
  • O(nlogn)       贪心 + 二分

>>思路:

  • 思路 1 不选      为了比大小,需要知道上一个选的数字
  • 思路 2 枚举选哪个     比较当前选得到数字和下一个要选的数字

>>举个栗子,比如[1,6,7,2,4,5,3]中:

子序列:就是从数组中选择一些数,且顺序和数组中的顺序是一致的。比如[2,5,3]就是这个数组的一个子序列

这题中的严格递增子序列,就是要求你选的子序列 右边的元素一定大于左边的元素。比如[1,2,5]就是一个严格递增的子序列。我们要做的就是在所有严格递增子序列中。找到最长的那个子序列的长度。比如[1,2,4,5]就是最长递增子序列。请注意是严格递增的,也就是说不能有相同元素,所以在示例3中,严格递增子序列只有一个元素,由于子序列本质上是数组的一个子集,我们可以考虑用子集型回溯来思考(O_O)?

对于子集型回溯,我们有「选或不选」 以及 「枚举选哪个」这两种思路,如果倒着思考,假设 3是子序列的最后一个数,考虑选或者不选的话,这前面的数字就需要和 3 比较大小,所以需要知道当前下标以外,还需要知道上一个数字的下标。

而如果考虑「枚举选哪个」,我们就可以直接枚举前面的比 小的数字,当做子序列的倒数第二个数。那么只需要知道当前所选的数字的下标就好了。

这样对比,会发现「枚举选哪个」只需要一个参数,比较好写。

(一)记忆化搜索 「思路一」

启发思路:枚举 nums[i] 作为 LIS 的末尾元素,那么需要枚举 nums[j] 作为 LIS 倒数第二个元素,其中 j < i 且 nums[j] < nums[i]

回溯三问:{

    ① 子问题? 以nums[i] 结尾的 LIS 长度

    ② 当前操作?枚举 nums[j]

    ③ 下一个子问题?以nums[j] 结尾的 LIS 长度

}
class Solution:
    #  记忆化搜索
    def lengthOfLIS(self, nums: List[int]) -> int:
        n = len(nums)
        @cache
        def dfs(i):
            res = 0
            for j in range(i):
                if nums[j] < nums[i]:
                    res = max(res,dfs(j))
            return res + 1
        # ans = 0
        # for i in range(n):
        #     ans = max(ans,dfs(i))
        # return ans
        return max(dfs(i) for i in range(n))
  • dfs(i) = max{dfs(j)} + 1   j < i 且 nums[j] < nums[i]
  • f[i] = max{f[j]} + 1           j < i 且 nums[j] < nums[i]

(二) 记忆化搜索,改成递推 「思路二」

class Solution:
    #  记忆化搜索 改成递推
    def lengthOfLIS(self, nums: List[int]) -> int:
        n = len(nums)
        f = [0] * n
        for i in range(n):
            for j in range(i):
                if nums[j] < nums[i]:
                    f[i] = max(f[i],f[j]);
            f[i] += 1;
        return max(f)

 (三)贪心 + 二分

「思路三」nums 的 LIS 等价于nums 与 排序去重后的 nums的LCS,例如 nums =  [1,3,3,2,4]。排序去重后 = [1,2,3,4]。LCS = [1,3,4] 或者 [1,2,4]

「思路四」考虑一个简单的贪心,如果我们要使上升子序列尽可能的长,则我们需要让序列上升得尽可能慢,因此我们希望每次在上升子序列最后加上的那个数尽可能的小。

  • 进阶技巧:交换状态与状态值
  • f[i] 表示末尾元素 为 nums[i] 的 LIS长度
  • g[i] 表示长度为 i+1 的IS的末尾元素的最小值
例如 nums = [1,6,7,2,4,5,3]
            g = [1]          // 第一步插入1, g = [1]
            g = [1,6]        // 第二步插入6, g = [1,6]
            g = [1,6,7]      // 第三步插入7, g = [1,6,7]
            g = [1,2,7]      // 第四步插入2, g = [1,2,7]
            g = [1,2,4]      // 第五步插入4, g = [1,2,4]
            g = [1,2,4,5]    // 第六步插入5, g = [1,2,4,5]
            g = [1,2,3,5]    // 第七步插入3, g = [1,2,3,5]

按照这种定义方式,由于没有重叠子问题,是不能算作动态规划的,而变成了一个贪心的问题,接着来研究一下g的性质,看上去 g 是一个严格递增的序列,并且每次要么添加一个数,要么修改一个数,这里就来严格证明一下,通常来说证明算法相关的一些结论,数学归纳法和反证法用的是最多的。这里就用反证法来证明,如果 g 不是严格递增的,比如说 g = [1,6,6] 那么最后的这个 6 肯定会对应一个长为 3 的,末尾为 6 的上升子序列,那第二个数是小于等于5的,而这就和第一个6矛盾了,它表示第二个数最小是6。所以通过反证法,我们可以得出 g 一定是一个严格递增的序列,知道 g 是严格递增的,就可以得出后面的结论了。

  • 推论1:一次只能更新一个位置

        证明:假设更新了两个位置,会导致 g 不是严格递增的,因为单调递增序列不能有相同元素

  • 推论2:更新的位置是第一个 >= nums[i]的数的下标

        如果nums[i] 比 g 的最后一个数都大,那就加到 g 的末尾

证明:设更新了 g[j],如果g[j] < nums[i],相当于把小的数给变大了,
    这显然不可能。另外,如果 g[j] 不是第一个 >= nums[i]的数,那就破坏
    了 g 的有序性
    g = [1,6,7],nums[i] = 2
      ↓
    g = [1,2,7]

算法g 上用二分查找快速找到第一个 >= nums[i] 的下标j,如果 j 不存在,那么nums[i]直接加到 g 末尾,否则修改 g[j] nums[i]

注意这个算法按分类的话,算「贪心 + 二分」

Python 代码: 

class Solution:
    # 贪心 + 二分
    def lengthOfLIS(self, nums: List[int]) -> int:
        g = []
        for x in nums:
            j = bisect_left(g,x)
            if j == len(g):
                g.append(x)
            else:
                g[j] = x
        return len(g)
  • 时间复杂度:O(nlogn)    
  • 空间复杂度:O(n)

 C++ 代码:

class Solution {
public:
    // 贪心 + 二分
    int lengthOfLIS(vector<int>& nums) {
        vector<int> g;
        for(int x:nums) {
            int j = lower_bound(g.begin(), g.end(), x) - g.begin();
            if(j == g.size()) g.push_back(x);
            else g[j] = x;
        }
        return g.size();
    }
};

思考:空间复杂度还能能进一步优化吗? 可以!!!

>>优化空间复杂度:O(1)

Python 代码:  

class Solution:
    # 贪心 + 二分 (优化空间复杂度) O(1)
    def lengthOfLIS(self, nums: List[int]) -> int:
        ng = 0
        for x in nums:
            j = bisect_left(nums,x,0,ng)
            if j == ng:
                nums[ng] = x
                ng+=1
            else:
                nums[j] = x
        return ng
  • 时间复杂度:O(nlogn)    
  • 空间复杂度:O(1)

 C++ 代码: 

class Solution {
public:
    // 贪心 + 二分 
    int lengthOfLIS(vector<int>& nums) {
        int ng = 0;
        for(int x:nums) {
            int j = lower_bound(nums.begin(), nums.begin() + ng, x) - nums.begin();
            if(j == ng) {
                nums[ng] = x;
                ng+=1;
            }
            else nums[j] = x;
        }
        return ng;
    }
};

>>拓展思考🤔

变形:如果LIS 中可以有相同元素呢?(非严格递增)那么g是非严格递增序列
在修改的是偶,和nums[i] 相同的 g[j] 就不同改了,而是修改 > nunms[i]
的第一个 g[j]
例如 nums = [1,6,7,2,2,5,2]
    g = [1]
    g = [1,6]
    g = [1,6,7]
    g = [1,2,7]
    g = [1,2,2]
    g = [1,2,2,5]
    g = [1,2,2,2]

要改的是大于2的第一个数,具体的证明方式和上面是一样的。对应到代码上,在python中就是把 bisect_left 改成 bisect_right,在C++中就是改成upper_bound

def lengthOfLIS(self, nums: List[int]) -> int:
        ng = 0
        for x in nums:
            j = bisect_right(nums,x,0,ng)
            if j == ng:
                nums[ng] = x
                ng+=1
            else:
                nums[j] = x
        return ng
class Solution {
public:
    // 贪心 + 二分 
    int lengthOfLIS(vector<int>& nums) {
        int ng = 0;
        for(int x:nums) {
            int j = upper_bound(nums.begin(), nums.begin() + ng, x) - nums.begin();
            if(j == ng) {
                nums[ng] = x;
                ng+=1;
            }
            else nums[j] = x;
        }
        return ng;
    }
};

>>参考和推荐视频: 

最长递增子序列【基础算法精讲 20】_哔哩哔哩_bilibiliicon-default.png?t=N7T8https://www.bilibili.com/video/BV1ub411Q7sB/?vd_source=a934d7fc6f47698a29dac90a922ba5a3

>>此题动态规划详解,可看我的往期文章:

leetCode 300.最长递增子序列 动态规划 + 图解_呵呵哒( ̄▽ ̄)"的博客-CSDN博客j 其实就是遍历 0 到 i-1,那么是从前向后,还是从后到前都可以,只要是 0 到 i-1 的元素都遍历了就可以,所以习惯从前向后遍历。dp[i] 是 由 0 到 i-1 各个位置的最长递增子序列 推导出来,那么遍历 i 一定是从前向后遍历。“子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序”dp[i]表示 i 之前包括 i 的以 nums[i] 结尾的最长递增子序列的长度。最长递增子序列是 [2,3,7,101],因此长度为 4。是数组 [0,3,1,6,2,2,7]https://blog.csdn.net/weixin_41987016/article/details/133636345?spm=1001.2014.3001.5501

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1067857.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Kafka 高可用

正文 一、高可用的由来 1.1 为何需要Replication 在Kafka在0.8以前的版本中&#xff0c;是没有Replication的&#xff0c;一旦某一个Broker宕机&#xff0c;则其上所有的Partition数据都不可被消费&#xff0c;这与Kafka数据持久性及Delivery Guarantee的设计目标相悖。同时Pr…

算法通关村第17关【青铜】| 贪心

贪心算法&#xff08;Greedy Algorithm&#xff09;是一种常见的算法设计策略&#xff0c;通常用于解决一类优化问题。其核心思想是&#xff1a;在每一步选择中都采取当前状态下的最优决策&#xff0c;从而希望最终能够达到全局最优解。贪心算法不像动态规划算法需要考虑各种子…

电暖产品经营小程序商城搭建

电暖产品的需求度很高&#xff0c;包括地暖系统及壁挂炉、水暖散热器等&#xff0c;尤其每年冬天&#xff0c;部分家庭或办公场所就会有相关需求&#xff0c;庞大市场下为电暖各领域商家及品牌带来了商机。 然而随着互联网深入各行业及实体店生意难做&#xff0c;无论品牌还是…

安卓RecycleView包含SeekBar点击列表底部圆形阴影处理

seekbar在列表中点击底部圆形阴影禁止显示方法 大家好&#xff0c;最近写了自定义的seekbar实现显示进度值&#xff0c;然而呢&#xff0c;我的seekbar控件是作为recycleview的item来使用的&#xff0c;我设置了禁止点击和滑动方法如下&#xff1a; seekBar.setOnTouchListene…

ubuntu使用whisper和funASR-语者分离-二值化

文章目录 一、选择系统1.1 更新环境 二、安装使用whisper2.1 创建环境2.1 安装2.1.1安装基础包2.1.2安装依赖 3测试13测试2 语着分离创建代码报错ModuleNotFoundError: No module named pyannote报错No module named pyannote_whisper 三、安装使用funASR1 安装1.1 安装 Conda&…

SpringBoot+MinIO实现对象存储

一、 MinIO MinIO 是一个基于Apache License v2.0开源协议的对象存储服务。它兼容亚马逊S3云存储服务接口&#xff0c;非常适合于存储大容量非结构化的数据&#xff0c;例如图片、视频、日志文件、备份数据和容器/虚拟机镜像等&#xff0c;而一个对象文件可以是任意大小&#…

【好玩】如何在github主页放一条贪吃蛇

前言 &#x1f34a;缘由 github放小蛇&#xff0c;就问你烧不烧 起因看到大佬github上有一条贪吃蛇扭来扭去&#xff0c;觉得好玩&#xff0c;遂给大家分享一下本狗的玩蛇历程 &#x1f95d;成果初展 贪吃蛇 &#x1f3af;主要目标 实现3大重点 1. github设置主页 2. git…

Arcgis日常天坑问题(1)——将Revit模型转为slpk数据卡住不前

这段时间碰到这么一个问题&#xff0c;revit模型在arcgis pro里导出slpk的时候&#xff0c;卡在98%一直不动&#xff0c;大约有两个小时。 首先想到的是revit模型过大&#xff0c;接近300M。然后各种减小模型测试&#xff0c;还是一样的问题&#xff0c;大概花了两天的时间&am…

OpenHarmony父子组件双项同步使用:@Link装饰器

子组件中被Link装饰的变量与其父组件中对应的数据源建立双向数据绑定。 说明&#xff1a; 从API version 9开始&#xff0c;该装饰器支持在ArkTS卡片中使用。 概述 Link装饰的变量与其父组件中的数据源共享相同的值。 装饰器使用规则说明 Link变量装饰器 说明 装饰器参数 无…

实现协议互通:探索钡铼BL124EC的EtherCAT转Ethernet/IP功能

钡铼BL124EC是一种用于工业网络通信的网关设备&#xff0c;专门用于将EtherCAT协议转换成Ethernet/IP协议。它充当一个桥梁&#xff0c;连接了使用不同协议的设备&#xff0c;使它们能够无缝地进行通信和互操作。 具体来说&#xff0c;BL124EC通过支持EtherCAT&#xff08;以太…

5SpringMVC处理Ajax请求携带的JSON格式(“key“:value)的请求参数

SpringMVC处理Ajax 参考文章数据交换的常见格式,如JSON格式和XML格式 请求参数的携带方式 浏览器发送到服务器的请求参数有namevalue&...(键值对)和{key:value,...}(json对象)两种格式 URL请求会将请求参数以键值对的格式拼接到请求地址后面,form表单的GET和POST请求会…

论文阅读——Large Selective Kernel Network for Remote Sensing Object Detection

目录 基本信息标题目前存在的问题改进网络结构另一个写的好的参考 基本信息 期刊CVPR年份2023论文地址https://arxiv.org/pdf/2303.09030.pdf代码地址https://github.com/zcablii/LSKNet 标题 遥感目标检测的大选择核网络 目前存在的问题 相对较少的工作考虑到强大的先验知…

HTML5+CSS3+移动web 前端开发入门笔记(二)HTML标签详解

HTML标签&#xff1a;排版标签 排版标签用于对网页内容进行布局和样式的调整。下面是对常见排版标签的详细介绍&#xff1a; <h1>: 定义一级标题&#xff0c;通常用于标题栏或页面主要内容的标题。<p>: 定义段落&#xff0c;用于将文字分段展示&#xff0c;段落之…

mysql面试题25:数据库自增主键可能会遇到什么问题?应该怎么解决呢?

该文章专注于面试,面试只要回答关键点即可,不需要对框架有非常深入的回答,如果你想应付面试,是足够了,抓住关键点 面试官:数据库自增主键可能会遇到什么问题? 数据库自增主键可能遇到的问题: 冲突问题:自增主键是通过自动递增生成的唯一标识符,但在某些情况下可能会…

Sentinel入门

文章目录 初始Sentinel雪崩问题服务保护技术对比认识Sentinel微服务整合Sentinel 限流规则快速入门流控模式关联模式链路模式 流控效果warm up排队等待 热点参数限流全局参数限流热点参数限流 隔离和降级FeignClient整合Sentinel线程隔离熔断降级慢调用异常比例、异常数 授权规…

MATLAB算法实战应用案例精讲-【优化算法】霸王龙优化算法(TROA)(附MATLAB代码实现)

前言 霸王龙优化算法(Tyrannosaurus optimization,TROA)由Venkata Satya Durga Manohar Sahu等人于2023年提出,该算法模拟霸王龙的狩猎行为,具有搜索速度快等优势。 霸王龙属于暴龙超科的暴龙属,是该属的唯一一种。1905年,美国古生物学家、美国艺术与科学院院士亨利奥…

iOS——仿写计算器

四则运算&#xff1a;中缀表达式转后缀表达式后缀表达式求值 实现四则运算的算法思路是&#xff1a;首先输入的是中缀表达式的字符串&#xff0c;然后将其转为计算机可以理解的后缀表达式&#xff0c;然后将后缀表达式求值&#xff1a; 中缀转后缀表达式思路参考&#xff1a;《…

竹云筑基,量子加密| 竹云携手国盾量子构建量子身份安全防护体系

9月23日-24日&#xff0c;2023量子产业大会在安徽合肥举行。作为量子科技领域行业盛会&#xff0c;2023年量子产业大会以“协同创新 量点未来”为主题&#xff0c;展示了前沿的量子信息技术、产业创新成果&#xff0c;并举办主旨论坛、量子科普讲座等系列专项活动。量子信息作为…

多种方案教你彻底解决mac npm install -g后仍然不行怎么办sudo: xxx: command not found

问题概述 某些时候我们成功执行了npm install -g xxx&#xff0c;但是执行完成以后&#xff0c;使用我们全局新安装的包依然不行&#xff0c;如何解决呢&#xff1f; 解决方案1&#xff1a; step1: 查看npm 全局文件安装地址 XXXCN_CXXXMD6M ~ % npm list -g …

45 二叉树的右视图

二叉树的右视图 题解1 层序遍历——BFS题解2 递归——DFS 给定一个二叉树的根节点 root&#xff0c;想象自己站在它的右侧&#xff0c;按照从顶部到底部的顺序&#xff0c;返回从右侧所能看到的节点值。 提示: 二叉树的节点个数的范围是 [0,100]-100 < Node.val < 100 …