代码随想录训练第二十六天|LeetCode452. 用最少数量的箭引爆气球、LeetCode435.无重叠区间、LeetCode763.划分字母区间
452. 用最少数量的箭引爆气球
有一些球形气球贴在一堵用 XY 平面表示的墙面上。墙面上的气球记录在整数数组 points
,其中points[i] = [xstart, xend]
表示水平直径在 xstart
和 xend
之间的气球。你不知道气球的确切 y 坐标。
一支弓箭可以沿着 x 轴从不同点 完全垂直 地射出。在坐标 x
处射出一支箭,若有一个气球的直径的开始和结束坐标为 x``start
,x``end
, 且满足 xstart ≤ x ≤ x``end
,则该气球会被 引爆 。可以射出的弓箭的数量 没有限制 。 弓箭一旦被射出之后,可以无限地前进。
给你一个数组 points
,返回引爆所有气球所必须射出的 最小 弓箭数 。
示例 1:
输入:points = [[10,16],[2,8],[1,6],[7,12]]
输出:2
解释:气球可以用2支箭来爆破:
-在x = 6处射出箭,击破气球[2,8]和[1,6]。
-在x = 11处发射箭,击破气球[10,16]和[7,12]。
示例 2:
输入:points = [[1,2],[3,4],[5,6],[7,8]]
输出:4
解释:每个气球需要射出一支箭,总共需要4支箭。
示例 3:
输入:points = [[1,2],[2,3],[3,4],[4,5]]
输出:2
解释:气球可以用2支箭来爆破:
- 在x = 2处发射箭,击破气球[1,2]和[2,3]。
- 在x = 4处射出箭,击破气球[3,4]和[4,5]。
提示:
1 <= points.length <= 105
points[i].length == 2
-231 <= xstart < xend <= 231 - 1
Related Topics
-
贪心
-
数组
-
排序
思路
如何使用最少的弓箭呢?
直觉上来看,貌似只射重叠最多的气球,用的弓箭一定最少,那么有没有当前重叠了三个气球,我射两个,留下一个和后面的一起射这样弓箭用的更少的情况呢?
尝试一下举反例,发现没有这种情况。
那么就试一试贪心吧!局部最优:当气球出现重叠,一起射,所用弓箭最少。全局最优:把所有气球射爆所用弓箭最少。
算法确定下来了,那么如何模拟气球射爆的过程呢?是在数组中移除元素还是做标记呢?
如果真实的模拟射气球的过程,应该射一个,气球数组就remove一个元素,这样最直观,毕竟气球被射了。
但仔细思考一下就发现:如果把气球排序之后,从前到后遍历气球,被射过的气球仅仅跳过就行了,没有必要让气球数组remove气球,只要记录一下箭的数量就可以了。
以上为思考过程,已经确定下来使用贪心了,那么开始解题。
为了让气球尽可能的重叠,需要对数组进行排序。
那么按照气球起始位置排序,还是按照气球终止位置排序呢?
其实都可以!只不过对应的遍历顺序不同,我就按照气球的起始位置排序了。
既然按照起始位置排序,那么就从前向后遍历气球数组,靠左尽可能让气球重复。
从前向后遍历遇到重叠的气球了怎么办?
如果气球重叠了,重叠气球中右边边界的最小值 之前的区间一定需要一个弓箭。
以题目示例: [[10,16],[2,8],[1,6],[7,12]]为例,如图:(方便起见,已经排序)
可以看出首先第一组重叠气球,一定是需要一个箭,气球3,的左边界大于了 第一组重叠气球的最小右边界,所以再需要一支箭来射气球3了。
public int findMinArrowShots(int[][] points) {
//先进行数组排序
// 根据气球直径的开始坐标从小到大排序
Arrays.sort(points, (a, b) -> {
return Integer.compare(a[0], b[0]);
});
int count = 1;
//int right = points[0][1];
for (int i = 1; i < points.length; i++) {
//如果当前节点的左边界大于上一个节点的右边界
//则需要新增一根箭
if (points[i][0] > points[i - 1][1]) {
count++;
}
//如果有重合部分
//则更新当前区间的最小右边界
else {
points[i][1] = Math.min(points[i][1], points[i - 1][1]);
}
}
return count;
}
- 时间复杂度:O(nlog n),因为有一个快排
- 空间复杂度:O(1),有一个快排,最差情况(倒序)时,需要n次递归调用。因此确实需要O(n)的栈空间
注意:题目中说的是:满足 xstart ≤ x ≤ xend,则该气球会被引爆。那么说明两个气球挨在一起不重叠也可以一起射爆,
所以代码中
if (points[i][0] > points[i - 1][1])
不能是>=
总结
寻找重复的气球,寻找重叠气球最小右边界,其实都有代码技巧。
435.无重叠区间
给定一个区间的集合 intervals
,其中 intervals[i] = [starti, endi]
。返回 需要移除区间的最小数量,使剩余区间互不重叠 。
示例 1:
输入: intervals = [[1,2],[2,3],[3,4],[1,3]]
输出: 1
解释: 移除 [1,3] 后,剩下的区间没有重叠。
示例 2:
输入: intervals = [ [1,2], [1,2], [1,2] ]
输出: 2
解释: 你需要移除两个 [1,2] 来使剩下的区间没有重叠。
示例 3:
输入: intervals = [ [1,2], [2,3] ]
输出: 0
解释: 你不需要移除任何区间,因为它们已经是无重叠的了。
提示:
1 <= intervals.length <= 105
intervals[i].length == 2
-5 * 104 <= starti < endi <= 5 * 104
Related Topics
-
贪心
-
数组
-
动态规划
-
排序
思路
相信很多同学看到这道题目都冥冥之中感觉要排序,但是究竟是按照右边界排序,还是按照左边界排序呢?
其实都可以。主要就是为了让区间尽可能的重叠。
我来按照右边界排序,从左向右记录非交叉区间的个数。最后用区间总数减去非交叉区间的个数就是需要移除的区间个数了。
此时问题就是要求非交叉区间的最大个数。
这里记录非交叉区间的个数还是有技巧的,如图:
区间,1,2,3,4,5,6都按照右边界排好序。
当确定区间 1 和 区间2 重叠后,如何确定是否与 区间3 也重贴呢?
就是取 区间1 和 区间2 右边界的最小值,因为这个最小值之前的部分一定是 区间1 和区间2 的重合部分,如果这个最小值也触达到区间3,那么说明 区间 1,2,3都是重合的。
接下来就是找大于区间1结束位置的区间,是从区间4开始。那有同学问了为什么不从区间5开始?别忘了已经是按照右边界排序的了。
区间4结束之后,再找到区间6,所以一共记录非交叉区间的个数是三个。
总共区间个数为6,减去非交叉区间的个数3。移除区间的最小数量就是3。
只要按照右边界进行排序,只要上一个的右边界,与下一个区间有重合的部分,说明需要去掉下一个区间
public int eraseOverlapIntervals(int[][] intervals) {
if (intervals.length == 0) {
return 0;
}
Arrays.sort(intervals, (a, b) -> {
return a[1] - b[1];
});
//记录当前未交叉的区间数量
int count = 1;
//记录当前分割点,也就是有边界
int end = intervals[0][1];
for (int i = 1; i < intervals.length; i++) {
//如果当期右边界,与当前区间的左边界没有重合的部分,则需要更改count,并且更新右边界
if (end <= intervals[i][0]) {
count++;
end = intervals[i][1];
}
//如果有重合部分,则直接舍去当前区间,啥也不用干
}
return intervals.length - count;
}
- 时间复杂度:O(nlog n) ,有一个快排
- 空间复杂度:O(n),有一个快排,最差情况(倒序)时,需要n次递归调用。因此确实需要O(n)的栈空间
补充1
左边界排序可不可以呢?
也是可以的,只不过 左边界排序我们就是直接求 重叠的区间,count为记录重叠区间数。
public int eraseOverlapIntervals(int[][] intervals) {
if (intervals.length == 0) {
return 0;
}
//使用左边界进行排序
Arrays.sort(intervals, (a, b) -> {
return a[0] - b[0];
});
//记录重叠区间的数量
int count = 0;
//记录上一个区间的右边界
int right = intervals[0][1];
for (int i = 1; i < intervals.length; i++) {
//没有重合部分
if (right <= intervals[i][0]) {
//更新右边界
right = intervals[i][1];
} else {
//记录数量
count++;
//更新右边界
right = Math.min(right, intervals[i][1]);
}
}
return count;
}
补充2
本题其实和452.用最少数量的箭引爆气球 (opens new window)非常像,弓箭的数量就相当于是非交叉区间的数量,只要把弓箭那道题目代码里射爆气球的判断条件加个等号(认为[0,1][1,2]不是相邻区间),然后用总区间数减去弓箭数量 就是要移除的区间数量了。
public int eraseOverlapIntervals(int[][] intervals) {
if (intervals.length == 0) {
return 0;
}
//使用右边界进行排序
Arrays.sort(intervals, (a, b) -> {
return a[1] - b[1];
});
//points不为空至少需要一支箭(也就是移除的区间数量)
int count = 1;
for (int i = 1; i < intervals.length; i++) {
//如果没有重合部分
if (intervals[i][0] >= intervals[i - 1][1]) {
//此时需要加一支箭
count++;
} else {
//修改当前右边界
intervals[i][1] = Math.min(intervals[i][1], intervals[i - 1][1]);
}
}
return intervals.length - count;
}
763.划分字母区间
给你一个字符串 s
。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。
注意,划分结果需要满足:将所有划分结果按顺序连接,得到的字符串仍然是 s
。
返回一个表示每个字符串片段的长度的列表。
示例 1:
输入:s = "ababcbacadefegdehijhklij"
输出:[9,7,8]
解释:
划分结果为 "ababcbaca"、"defegde"、"hijhklij" 。
每个字母最多出现在一个片段中。
像 "ababcbacadefegde", "hijhklij" 这样的划分是错误的,因为划分的片段数较少。
示例 2:
输入:s = "eccbbbbdec"
输出:[10]
提示:
1 <= s.length <= 500
s
仅由小写英文字母组成
Related Topics
-
贪心
-
哈希表
-
双指针
-
字符串
思路
一想到分割字符串就想到了回溯,但本题其实不用回溯去暴力搜索。
题目要求同一字母最多出现在一个片段中,那么如何把同一个字母的都圈在同一个区间里呢?
如果没有接触过这种题目的话,还挺有难度的。
在遍历的过程中相当于是要找每一个字母的边界,如果找到之前遍历过的所有字母的最远边界,说明这个边界就是分割点了。此时前面出现过所有字母,最远也就到这个边界了。
可以分为如下两步:
- 统计每一个字符最后出现的位置
- 从头遍历字符,并更新字符的最远出现下标,如果找到字符最远出现位置下标和当前下标相等了,则找到了分割点
如图:
public List<Integer> partitionLabels(String s) {
//创建一个数组,找到
int[] hash = new int[27];
//统计每一个字符出现的最后位置
for (int i = 0; i < s.length(); i++) {
hash[s.charAt(i) - 'a'] = i;
}
List<Integer> res = new ArrayList<>();
int left = 0;
int right = 0;
for (int i = 0; i < s.length(); i++) {
//找到字符出现的最远边界
//也就是更新字符出现的最远边界
right = Math.max(right, hash[s.charAt(i) - 'a']);
//如果到达了当前字符串的最远边界,则进行记录,并且更新区间
if (i == right) {
res.add(right - left + 1);
left = i + 1;
}
}
return res;
}
- 时间复杂度:O(n)
- 空间复杂度:O(1),使用的hash数组是固定大小
原文地址:https://blog.csdn.net/weixin_60364343/article/details/140576373
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!