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

news/2024/10/22 12:35:47/

文章目录

  • 最长递增子序列
    • 解法一:动态规划
    • 解法二: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.ppmy.cn/news/729455.html

相关文章

VIM编辑器常用快捷键

前提说明&#xff1a; &#xff0c;代表输入后可等待如g,g代表连续输入两个g中间可停留&#xff1b; |代表或&#xff0c;即多种快捷键均可实现&#xff1b; 快捷键之间可以自己尝试进行组合&#xff0c;说不定就可以&#xff01;&#xff01;&#xff01; 光标移动 快捷键…

应用商店上架被拒解决办法

App Store上架被拒解决办法 很多开发者在开发完项目&#xff0c;发布应用市场的过程中面临着审核被拒的问题。如果说一开始就能规避这些问题&#xff0c;那么对项目的规划、运营、推广将会产生极大有效的推进。 本篇文章主要描述是针对上架App Store所常见的一些被拒原因以…

和宝贝教师版v1.1.4官方iPhone版

和宝贝教师版v1.1.4官方iPhone版 版本&#xff1a;1.1.4 大小&#xff1a;19.1MB软件语言&#xff1a;简体中文 软件类型&#xff1a;生活服务软件授权&#xff1a;免费版 系统要求&#xff1a;iOS7.0 软件简介 和宝贝教师版app是一款专为教师打造的应用&#xff0c;和宝贝教…

实现类似iphone手机删除应用程序的抖动效果

其实原理很简单&#xff0c;让图片向左&#xff0c;再向右旋转一定角度就可以啦 static BOOL wobblesLeft NO; UIButton * button[UIButton buttonWithType:UIButtonTypeCustom]; [button setImage:[UIImage imageNamed:"宝宝日记_1.png"] forState:UICon…

获取IOS应用安装列表

1.openURL 我们知道可以给应用设置URL Scheme&#xff0c;这样别的应用就可以通过这个地址打开咱们的应用。其实还有一个api叫canOpenURL.这样如果咱们知道要检查的IOS应用列表的URL Scheme的话&#xff0c;就可以用canOpenURL检查一下。 2.获取运行程序列表 // .h interface U…

安全测试(六)iOS ipa软件安全 APP应用安全 手机软件安全 ipa安全 ipa反编译 应用日志窃取 ipa漏洞 应用软件本身功能漏洞 iPhone移动应用常规安全讲解

目录 一、前言 二、iOS ipa软件安全 1、ipa 简介 2、ipa 提取 2.1 工具提取 失败案例 2.2 转包软件提取 失败案例 2.3 日志分析捕获 失败案例 2.4 内测二维码分享 成功案例 3、ipa Info.plist 配置文件分析 4、ipa日志分析 4.1 Windows下爱思助手 4.2 Windows下开源工具 …

iPhone丢了怎么找回

今天无意之间上网冲浪的时候&#xff0c;无意看到iCloud云的强大功能&#xff0c;想到了iPhone中的Find My iPhone功能。 熟悉使用苹果iPhone手机或者iPad设备的朋友&#xff0c;相信对IOS系统里的Find My iPhone 功能比较了解吧。Find My iPhone能够帮我们找回丢失/被盗的iPh…

软件创富密码:iPhone应用程序开发攻略之iPhone特色传感器应用(双色)

软件创富密码&#xff1a;iPhone应用程序开发攻略之iPhone特色传感器应用(双色) 王志刚等 编著 ISBN978-7-121-14440-0 2011年9月出版 定价&#xff1a;69.00元 16开 288页 内 容 简 介 本书集中介绍了如何使用iPhone SDK提供的传感器API开发特色传感器应用程序&…