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

文章目录

  • 最长递增子序列
    • 解法一:动态规划
    • 解法二: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 <= 10^5
1 <= obstacles[i] <= 10^7

实际上是求以 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.hqwc.cn/news/4288.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

理解mysql数据库

1.MySQL 在 Centos 7环境安装 1.1 卸载不要的环境 ps ajx |grep mariadb # 先检查是否有 mariadb 存在 systemctl stop mariadb.service # 停⽌ mariadb 服务 ps ajx |grep mariadb # 再 检查是否有 mariadb 存在 1.2 删除多余的安装包 rpm -qa | grep mysql #查看默认安装…

Unity内置渲染管线升级URP教程

简介 URP全称为Universal Render Pipeline(通用渲染管线)&#xff0c;可以提供更加灵活的渲染方案&#xff0c;通过添加Render Feature实现各种渲染效果。并且可以针对移动平台进行专门的优化&#xff0c;同时还提供了SRPBatcher提高渲染效率。Unity的一些工具&#xff0c;比如…

《项目实战》构建SpringCloud alibaba项目(三、构建服务方子工程store-user-service)

系列文章目录 构建SpringCloud alibaba项目&#xff08;一、构建父工程、公共库、网关&#xff09; 构建SpringCloud alibaba项目&#xff08;二、构建微服务鉴权子工程store-authority-service&#xff09; 构建SpringCloud alibaba项目&#xff08;三、构建服务方子工程stor…

95道MongoDB面试题

1、mongodb是什么&#xff1f; MongoDB 是由 C语言编写的&#xff0c;是一个基于分布式文件存储的开源数据库系统。 再高负载的情况下&#xff0c;添加更多的节点&#xff0c;可以保证服务器性能。 MongoDB 旨在给 WEB 应用提供可扩展的高性能数据存储解决方案。 MongoDB 将数…

前端技术栈 - ES6 - Promise -模块化编程

文章目录 &#x1f55b;ES6⚡let变量⚡const常量⚡数组解构⚡对象解构⚡模板字符串⚡对象声明简写⚡对象方法简写⚡对象运算符扩展⚡箭头函数⚡作业布置 &#x1f550;Promise⚡需求分析⚡传统ajax回调嵌套⚡promise改进⚡promise异常捕获⚡promise链式调用⚡promise代码重排优…

Spark 3.4.x Server Client模式下的数据传输实现

背景 在Spark中python和jvm的通信杂谈–ArrowConverter中&#xff0c;我们提到Spark 3.4.x中是Client和Server之间的数据传输是采用Arrow IPC的&#xff0c;那具体是怎么实现的呢&#xff1f; 分析 直接上代码ClientE2ETestSuite test("createDataFrame from complex t…

Redis————主从架构

主从架构搭建 单机多实例 粗制一份redis.conf文件 将相关配置修改为如下值&#xff1a; port 与主节点端口后不相同即可 pidfile pid进程号保存文件pidfile的路径 logfile 日志文件名称 dir 指定数据存放目录 #需要注释掉bind #bind 127.0.0.1&#xff08;bind绑定的是自己机…

Git使用与配置

Git分布式版本控制工具 一、Git安装与配置 Git基本配置 打开Git Bash 设置用户信息 # 配置用户名和用户邮箱 git config --global user.name xxx git config --global user.email xxxxx.com查看用户信息 # 查看用户名和用户邮箱 git config --global user.name git config --g…

【前端|CSS系列第1篇】CSS零基础入门之CSS的基本概念和选择器

欢迎来到CSS零基础入门系列的第一篇博客&#xff01;在这个系列中&#xff0c;我们将一起学习CSS&#xff08;层叠样式表&#xff09;的基础知识&#xff0c;探索如何为网页添加样式和布局。本篇博客将重点介绍CSS的基本概念和选择器&#xff0c;帮助你理解CSS的核心概念。 1.…

【数据结构OJ题】链表带环问题

目录 1.判断链表是否带环 证明 2.寻找环的入口点 1.判断链表是否带环 原题链接&#xff1a;力扣 思路一&#xff1a;先遍历一遍链表&#xff0c;计算出链表的长度&#xff0c;然后将长度除二&#xff0c;在遍历半个链表即可。但是这种方法效率比较低。 思路二&#xff1a;…

学习一波Java语言中的优先队列 PriorityQueue

目录 一、什么是优先队列 二、PriorityQueue 如何使用 三、优先队列的使用场景 一、什么是优先队列 优先队列是一种特殊的队列数据结构&#xff0c;它根据元素的优先级来确定元素的顺序。与普通队列不同的是&#xff0c;优先队列中的元素并不按照插入的先后顺序进行排列&am…

线性代数基础--向量

目录 向量的概念 基本概念 抽象概念 向量的意义 几何意义 物理意义 欧式空间 特点和性质 行向量与列向量 行向量 列向量 两者的关系 向量的基本运算与范数 向量的基本运算 向量的加法 数乘运算&#xff08;实数与向量相乘&#xff09; 转置 向量的范数 向量…