【算法】最长递增子序列:动态规划贪心+二分查找

news2024/9/20 8:51:08

文章目录

  • 最长递增子序列
    • 解法一:动态规划
    • 解法二:LIS 和 LCS 的关系
    • 解法三:贪心 + 二分查找
  • 相关题目
    • 673. 最长递增子序列的个数 https://leetcode.cn/problems/number-of-longest-increasing-subsequence/
    • 1964. 找出到每个位置为止最长的有效障碍赛跑路线 https://leetcode.cn/problems/find-the-longest-valid-obstacle-course-at-each-position/
    • 1671. 得到山形数组的最少删除次数 https://leetcode.cn/problems/minimum-number-of-removals-to-make-mountain-array/
    • 354. 俄罗斯套娃信封问题 https://leetcode.cn/problems/russian-doll-envelopes/
    • 1626. 无矛盾的最佳球队 https://leetcode.cn/problems/best-team-with-no-conflicts/

本文介绍最长递增子序列的两种解法,以及一些相关题目的简单答案。


本文的重点是学习 时间复杂度为 O ( N 2 ) O(N^2) O(N2) 的动态规划时间复杂度为 ( N ∗ log ⁡ 2 N ) (N*\log{2}{N}) (Nlog2N) 的贪心+二分查找 这两种解决 类 最长递增子序列问题的解法。

在最后补充的相关题目中,需要学习当需要考虑的元素有两个时,如何通过自定义排序来避免考虑其中的一个元素

最长递增子序列

300. 最长递增子序列
在这里插入图片描述

解法一:动态规划

双重 for 循环 dp。

class Solution {
    public int lengthOfLIS(int[] nums) {
        int n = nums.length, ans = 1;
        int[] dp = new int[n];
        Arrays.fill(dp, 1);
        for (int i = 0; i < n; ++i) {
            for (int j = 0; j < i; ++j) {
            	// nums[i]可以作为nums[j]的后续元素
                if (nums[i] > nums[j]) dp[i] = Math.max(dp[i], dp[j] + 1);
            }
            ans = Math.max(ans, dp[i]);
        }
        return ans;
    }
}

还有一种 dp 写法,我感觉比较奇怪还没有理解:

class Solution {
    public int lengthOfLIS(int[] nums) {
        int n = nums.length, ans = 1;
        int[] dp = new int[n];
        Arrays.fill(dp, 0);
        for (int i = 0; i < n; ++i) {
            for (int j = 0; j < i; ++j) {
                if (nums[i] > nums[j]) dp[i] = Math.max(dp[i], dp[j]);
            }
            dp[i]++;
        }
        return Arrays.stream(dp).max().getAsInt();
    }
}

推荐使用第一种写法。

解法二:LIS 和 LCS 的关系

在这里插入图片描述
也就是说:
nums = [1, 3, 3, 2, 4]
排序去重后为: [1, 2, 3, 4]
求 nums 和 [1, 2, 3, 4] 的最长公共子序列就好了。方法参见:【算法】最长公共子序列&编辑距离

这种方法 和 上面的 DP 方法的时间复杂度都是 O ( n 2 ) O(n^2) O(n2) 的。

解法三:贪心 + 二分查找

进阶技巧:对于动态规划,可以尝试 交换状态与状态值
在这里插入图片描述
例如:
在这里插入图片描述

很容易可以理解下面代码的逻辑,从前向后依次遍历各个元素。

  • 当前元素大于列表中已有的最后一个元素时,将其加入列表;
  • 当前元素不大于列表中已有的最后一个元素时,则找到列表中第一个大于等于当前元素数字的位置,将其替换成当前元素。
class Solution {
    public int lengthOfLIS(int[] nums) {
        List<Integer> ls = new ArrayList();
        int n = nums.length;
        ls.add(nums[0]);
        for (int i = 1; i < n; ++i) {
            if (nums[i] > ls.get(ls.size() - 1)) ls.add(nums[i]);
            else {
                int l = 0, r = ls.size() - 1;   // 找到第一个大于等于nums[i]的位置
                while (l < r) {
                    int mid = l + r >> 1;
                    if (ls.get(mid) < nums[i]) l = mid + 1;
                    else r = mid;
                }
                ls.set(l, nums[i]);
            }
        }
        return ls.size();
    }
}

相关题目

673. 最长递增子序列的个数 https://leetcode.cn/problems/number-of-longest-increasing-subsequence/

https://leetcode.cn/problems/number-of-longest-increasing-subsequence/

在这里插入图片描述
这道题目不仅需要知道最长递增子序列的长度,还需要知道它的数量,因此需要一个额外的 cnt[] 数组。

class Solution {
    public int findNumberOfLIS(int[] nums) {
        int n = nums.length, mxL = 1, ans = 0;
        int[] dp = new int[n], cnt = new int[n];    // 一个记录最大长度,一个记录最大长度的数量
        Arrays.fill(dp, 1);
        Arrays.fill(cnt, 1);
        for (int i = 0; i < n; ++i) {
            for (int j = 0; j < i; ++j) {
            	// dp 递推
                if (nums[i] > nums[j]) {
                    if (dp[j] + 1 > dp[i]) {
                        dp[i] = dp[j] + 1;
                        cnt[i] = cnt[j];
                    } else if (dp[j] + 1 == dp[i]) cnt[i] += cnt[j];
                }
            }
            mxL = Math.max(mxL, dp[i]);
        }
        // 统计所有序列长度为最长的数量之和
        for (int i = 0; i < n; ++i) {
            if (dp[i] == mxL) ans += cnt[i];
        }
        return ans;
    }
}

这题也可以使用 贪心+前缀和+二分查找 来做。(有兴趣的自己看吧:https://leetcode.cn/problems/number-of-longest-increasing-subsequence/solution/zui-chang-di-zeng-zi-xu-lie-de-ge-shu-by-w12f/)

1964. 找出到每个位置为止最长的有效障碍赛跑路线 https://leetcode.cn/problems/find-the-longest-valid-obstacle-course-at-each-position/

https://leetcode.cn/problems/find-the-longest-valid-obstacle-course-at-each-position/
在这里插入图片描述

提示:
n == obstacles.length
1 <= n <= 105
1 <= obstacles[i] <= 107

实际上是求以 i 为结尾的最长非递减子序列长度。

观察到题目给出的数据范围,因此直接使用时间复杂度为 O ( N 2 ) O(N^2) O(N2)的动态规划是会TLE的。
下面给出超时的代码:

class Solution {
    public int[] longestObstacleCourseAtEachPosition(int[] obstacles) {
        int n = obstacles.length;
        int[] ans = new int[n];
        Arrays.fill(ans, 1);
        for (int i = 0; i < n; ++i) {
            for (int j = i - 1; j >= 0; --j) {
                if (obstacles[j] <= obstacles[i]) ans[i] = Math.max(ans[j] + 1, ans[i]);
            }
        }
        return ans;
    }
}

所以,我们需要使用时间复杂度为 O ( N ∗ log ⁡ 2 N ) O(N*\log_{2}{N}) O(Nlog2N) 的贪心+二分查找方法来做这道题目。
代码如下:

class Solution {
    public int[] longestObstacleCourseAtEachPosition(int[] obstacles) {
        int n = obstacles.length;
        List<Integer> ls = new ArrayList();
        ls.add(obstacles[0]);
        int[] ans = new int[n];
        ans[0] = 1;
        for (int i = 1; i < n; ++i) {
            if (obstacles[i] >= ls.get(ls.size() - 1)) {	// 很大,直接放在最后
                ls.add(obstacles[i]);
                ans[i] = ls.size();
            } else {
                // 寻找第一个大于obstacles[i]的数字
                int l = 0, r = ls.size() - 1;
                while (l < r) {
                    int mid = l + r >> 1;
                    if (ls.get(mid) <= obstacles[i]) l = mid + 1;
                    else r = mid;
                }
                ls.set(l, obstacles[i]);
                ans[i] = l + 1;
            }
        }
        return ans;
    }
}

将代码与 最长递增子序列 这道题目的答案进行比较,可以发现其实只多了两句:

ans[i] = ls.size();
和
ans[i] = l + 1;

1671. 得到山形数组的最少删除次数 https://leetcode.cn/problems/minimum-number-of-removals-to-make-mountain-array/

https://leetcode.cn/problems/minimum-number-of-removals-to-make-mountain-array/

在这里插入图片描述

对于数组中的每个元素,求 以它为结尾的从前往后的最长递增子序列长度以它为结尾的从后往前的最长递增子序列的长度,这样它就是山形数组的山顶。

判断哪个元素作为山顶时,两个递增子序列的长度之和最长,结果就取哪个。

(注意题目要求山顶两边都必须有比它小的数字)

class Solution {
    public int minimumMountainRemovals(int[] nums) {
        int n = nums.length, ans = n;
        int[] l1 = new int[n], l2 = new int[n];
        List<Integer> ls = new ArrayList();
        // 找从前往后的
        for (int i = 0; i < n; ++i) {
            if (ls.size() == 0 || nums[i] > ls.get(ls.size() - 1)) ls.add(nums[i]);
            else ls.set(bs(ls, nums[i]), nums[i]);
            l1[i] = ls.size();
        }
        ls.clear();
        // 找从后往前的
        for (int i = n - 1; i >= 0; --i) {
            if (ls.size() == 0 || nums[i] > ls.get(ls.size() - 1)) ls.add(nums[i]);
            else ls.set(bs(ls, nums[i]), nums[i]);
            l2[i] = ls.size();
        }
        for (int i = 0; i < n; ++i) {
            // 山顶两边都必须有比它小的数字,因此序列长度只有1(只有它自己)是不行的
            if (l1[i] == 1 || l2[i] == 1) continue;     
            ans = Math.min(n - l1[i] - l2[i] + 1, ans);
        }
        return ans;
    }

    public int bs(List<Integer> ls, int v) {
        // 二分查找
        int l = 0, r = ls.size() - 1;
        while (l < r) {
            int mid = l + r >> 1;
            if (v > ls.get(mid)) l = mid + 1;
            else r = mid;
        }
        return l;
    }
}

354. 俄罗斯套娃信封问题 https://leetcode.cn/problems/russian-doll-envelopes/

https://leetcode.cn/problems/russian-doll-envelopes/

在这里插入图片描述
提示
1 <= envelopes.length <= 10^5
envelopes[i].length == 2
1 <= wi, hi <= 10^5

注意看数据范围,使用 O ( N 2 ) O(N^2) O(N2) 的动态规划是会超时的。

这道题目一个很牛逼的点在于:使用自定义排序,这样在遍历的过程中就可以忽略信封的宽度了。
忽略宽度后,求排序后高度的最长递增子序列即可。

class Solution {
    public int maxEnvelopes(int[][] envelopes) {
        Arrays.sort(envelopes, (a, b) -> {
            return a[0] == b[0]? b[1] - a[1]: a[0] - b[0];  // 第一元素升序,第二元素降序
        });
        // 之后可以忽略第一元素了
        int n = envelopes.length;
        List<Integer> ls = new ArrayList();
        ls.add(envelopes[0][1]);
        for (int i = 1; i < n; ++i) {
            if (envelopes[i][1] > ls.get(ls.size() - 1)) ls.add(envelopes[i][1]);

            // 二分查找寻找需要放置的位置
            int l = 0, r = ls.size() - 1;
            while (l < r) {
                int mid = l + r >> 1, v = ls.get(mid);
                if (v < envelopes[i][1]) l = mid + 1;
                else r = mid;
            }
            ls.set(l, envelopes[i][1]);
        }
        return ls.size();
    }
}

Q:为什么要这样自定义排序?
A:首先按第一元素升序排序没有疑问,为了在遍历的过程中可以忽略第一元素,所以在第一元素相等的情况下,需要对第二元素进行降序排序。举个例子如下:
>
对第二关键字进行降序排序后,这些 h 值就不可能组成长度超过 1 的严格递增的序列了。
(详情解释可见:https://leetcode.cn/problems/russian-doll-envelopes/solution/e-luo-si-tao-wa-xin-feng-wen-ti-by-leetc-wj68/)

1626. 无矛盾的最佳球队 https://leetcode.cn/problems/best-team-with-no-conflicts/

https://leetcode.cn/problems/best-team-with-no-conflicts/

在这里插入图片描述

1 <= scores.length, ages.length <= 1000

数据范围比较小,可以使用时间复杂度为 O ( N 2 ) O(N^2) O(N2) 的动态规划。

对于这种需要同时考虑两种元素的,我们的一个重要策略就是通过自定义排序忽略其中的每一种元素。

在这道题中,先按分数升序排,再按年龄升序排。这样后面遍历到的分数已经时符合条件的,这样只需要判断年龄就可以了。

dp 数组的意义是:dp[i] 表示最后组建的球队中的最大球员序号为排序后的第 i 名球员时的球队最大分数(此时的球员序号为排序后的新序号)

class Solution {
    public int bestTeamScore(int[] scores, int[] ages) {
        int n = scores.length;
        int[][] people = new int[n][2];
        for (int i = 0; i < n; ++i) {
            people[i][0] = scores[i];
            people[i][1] = ages[i];
        }
        // 排序  按分数升序排,再按年龄升序排
        Arrays.sort(people, (a, b) -> {
            return a[0] != b[0]? a[0] - b[0]: a[1] - b[1];
        });
        int[] dp = new int[n];
        int ans = 0;
        for (int i = 0; i < n; ++i) {
            dp[i] = people[i][0];   // 至少选自己
            for (int j = 0; j < i; ++j) {
                // 我的分数一定大于等于你了,只要年纪也大于等于你就可以和你一起选
                if (people[i][1] >= people[j][1]) {
                    dp[i] = Math.max(dp[i], dp[j] + people[i][0]);
                }
            }
            ans = Math.max(ans, dp[i]);
        }
        return ans;
    }
}

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

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

相关文章

29.组件库 Element UI

Element UI是PC端常用的组件库&#xff0c;支持vue2与vue3&#xff0c;vue2项目使用的叫 Element UI,vue3使用的叫 Elements Plus&#xff0c;官网地址 一个 Vue 3 UI 框架 | Element Plus 我们下面的代码都是以vue3为例 目录 1 安装 2 引入 3 使用 1 安装 2 引入 完…

快消EDI:联合利华Unilever EDI需求分析

联合利华&#xff08;Unilever&#xff09;是一家跨国消费品公司&#xff0c;总部位于英国和荷兰&#xff0c;在全球范围内经营着众多知名品牌&#xff0c;涵盖了食品、饮料、清洁剂、个人护理产品等多个领域。作为一家跨国公司&#xff0c;联合利华在全球各地都有业务和生产基…

el-table表单一键展开折叠,展开部分后一键全部展开或折叠

实现功能&#xff1a; 1.表单一键展开或者一键折叠 2.表单点击展开一部分后&#xff0c;再次点击展开或折叠按钮可以全部展开或全部折叠 3.完整代码在最后 1.建立el-table的树形结构 1.ref"table"&#xff0c;用节点绑定的方式实现 2.data&#xff1a;树形结构…

Redis系列--数据过期清除策略缓存淘汰策略

一、过期策略 一、前言 Redis 所有的数据结构都可以设置过期时间&#xff0c;时间一到&#xff0c;就会自动删除。可以想象里面有一个专门删除过期数据的线程&#xff0c;数据已过期就立马删除。这个时候可以思考一下&#xff0c;会不会因为同一时间太多的 key 过期&#xff0…

windows电脑设置每天自动关机

有时候我们需要我们的笔记本或者电脑在每天固定的时间自动关机&#xff0c;但是windows本身是没有带这个设置的&#xff0c;下面记录下如何设置电脑每天自动关机&#xff0c;无需安装任何第三方软件&#xff1b; 文章目录 一、设置自动关机程序二、取消自动关机三、Windows任务…

chatgpt赋能python:Python连接表

Python连接表 Python作为一种高级编程语言&#xff0c;可以用于各种各样的任务。其中之一就是连接表格数据。连接表是在数据管理中非常重要的概念&#xff0c;因为它可以将不同表格中的数据合并在一起&#xff0c;从而使我们能够更好地分析和理解数据。在这篇文章中&#xff0…

蓝桥杯专题-试题版含答案-【猜算式】【排列序数】【还款计算】【滑动解锁】

点击跳转专栏>Unity3D特效百例点击跳转专栏>案例项目实战源码点击跳转专栏>游戏脚本-辅助自动化点击跳转专栏>Android控件全解手册点击跳转专栏>Scratch编程案例点击跳转>软考全系列点击跳转>蓝桥系列 &#x1f449;关于作者 专注于Android/Unity和各种游…

探索Gradio的CheckboxGroup模块:交互式多选框组件

❤️觉得内容不错的话&#xff0c;欢迎点赞收藏加关注&#x1f60a;&#x1f60a;&#x1f60a;&#xff0c;后续会继续输入更多优质内容❤️ &#x1f449;有问题欢迎大家加关注私戳或者评论&#xff08;包括但不限于NLP算法相关&#xff0c;linux学习相关&#xff0c;读研读博…

详解金融机构敏捷营销落地:体系架构、关键技术和实施方法

本文根据 2023 年金融营销科技价值发现论坛中&#xff0c;神策数据金融首席架构师王仕的主题演讲整理所得&#xff0c;聚焦营销 5.0 理念下的敏捷营销&#xff0c;详细讲解金融机构落地时涉及到的体系架构、关键技术及实施方法。 根据国家发改委官网披露的数据&#xff0c;2022…

macOS编译AirMap开源全景图源码image-processing

1.克隆源码 git clone --recursive https://github.com/airmap/image-processing.git 2. 使用CLion打开CMakeLists.txt并做为工程打开 2.默认配置名为Default,可修改,下面的所有配置项都可改 3.点击OK后会自动生成

青翼自研--4通道DAC播放子卡产品数据手册

FMC131是一款4通道3GSPS采样率或者2通道6GSPS采样率16位DA播放FMC子卡模块&#xff0c;该板卡为FMC标准&#xff0c;符合VITA57.4规范&#xff0c;可以作为一个理想的IO模块耦合至FPGA前端&#xff0c;16通道的JESD204B接口通过FMC连接器连接至FPGA的高速串行端口GTH。插值模式…

突破性5G NTN技术,美格智能携手高通发布卫星物联网连接方案

通信技术的快速发展&#xff0c;使得万物互联成为现实&#xff0c;物联网深刻影响我们的生活方式。目前&#xff0c;全球物联网连接主要由WiFi、蓝牙和蜂窝网络等几类技术支撑。数据显示&#xff0c;蜂窝基站的陆地覆盖率约为20%&#xff0c;而海洋覆盖率则不到5%。 这意味着陆…

十大编程算法

算法一&#xff1a;快速排序算法 快速排序是由东尼霍尔所发展的一种排序算法。在平均状况下&#xff0c;排序 n 个项目要Ο(n log n)次比较。在最坏状况下则需要Ο(n2)次比较&#xff0c;但这种状况并不常见。事实上&#xff0c;快速排序通常明显比其他Ο(n log n) 算法更快&a…

uni-app/vue 文字转语音朗读(附小程序语音识别和朗读)

语音播报的实现的方法有很多种&#xff0c;我这里介绍集中不引用百度、阿里或者迅飞的API的实现方式。 一、采用new SpeechSynthesisUtterance的方式 废话不多说直接上代码 data() {return {utterThis:null,} },//方法使用this.utterThis new SpeechSynthesisUtterance(); …

地下水管三维可视化综合管控平台提升政府服务质量

随着当前互联网技术数据的快速发展和增长&#xff0c;总数越来越大&#xff0c;结构越来越复杂。如果你想更清楚、更快地理解和理解数据&#xff0c;传统的二维平面图数据图性能不能满足需求。 什么是三维可视化? 三维可视化是一种利用计算机技术&#xff0c;再现三维世界中的…

命令模式(Command)

别名 动作&#xff08;Action&#xff09;事务&#xff08;Transaction&#xff09; 定义 命令是一种行为设计模式&#xff0c;它可将请求转换为一个包含与请求相关的所有信息的独立对象。该转换让你能根据不同的请求将方法参数化、延迟请求执行或将其放入队列中&#xff0c…

(1)深度学习学习笔记-数据操作和处理

文章目录 前言一、张量操作二、csv文件数据操作数据预处理(读入csv文件作为pytorch能处理的) 来源 前言 张量的一些处理和操作 csv文件数据操作 一、张量操作 [&#xff1a;&#xff0c;1]表示全部行 第二列 [&#xff1a;]是全取 [1:3,1&#xff1a;]&#xff1a;1:3表示1~3的…

Node.js搭建Https服务

要搭建一个接收HTTPS请求的Node.js服务器,可以按以下步骤操作: 1. 初始化项目和安装依赖: bashnpm init -y npm install expresslatest npm install httpslatest 2. 生成证书文件: 这里按照自己的需求 去买ssl证书 或者获取免费证书 直接百度 阿里云获取免费证书等关键词即可…

Prefix-Tuning 阅读笔记

《Prefix-Tuning: Optimizing Continuous Prompts for Generation》 核心思想&#xff1a; 微调的时候&#xff0c;把预训练好的大transformer固定住不训练&#xff0c;在大transformer前面拼接几个token的参数&#xff0c;只训练前面这几个token的参数。 为什么有效 因为虽…

金融科技领先者Broadridge选择CloudBees CI来加速软件交付

Broadridge公司是全球金融科技领先者&#xff0c;通过丰富客户参与度、控制风险、优化效率和创造收入增长的解决方案为客户推动业务转型。 借助CloudBees CI&#xff0c;Broadridge为所有使用Jenkins的开发团队提供了集中管理和自助服务的体验。Broadridge能够不断为客户提供新…