目录
一、做题心得
今天接触的是用贪心解决重叠区间问题。重叠区间问题首先就是要理解区间的范围:从起始点start开始到结束点end结束,这就构成一个区间。这类问题通常要求判断给定的区间集合中是否存在至少一个区间与其他区间重叠,进而完成后续的操作。如何实现这样的操作呢?这里就会运用倒排序,排序后,就能通过相邻区间找到重叠部分。
解决重叠区间这类题最常用的方法就是排序+贪心。
二、题目与题解
题目一:452. 用最少数量的箭引爆气球
题目链接
452. 用最少数量的箭引爆气球 - 力扣(LeetCode)
有一些球形气球贴在一堵用 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
题解:排序+贪心
作为重叠区间问题开始的第一道打卡题,这道题运用到的思想还是很经典的。
这道题的关键就是会找重叠区间。
首先就是排序,我们既可以按照气球的起始位置排序,也可以按照气球的结束位置排序,为了方便理解和应用,我们都选择按照起始位置排序。排完序之后,我们就能将位置相邻的区间数组下标也相邻--此时,重叠区间一定相邻。(可画图辅助理解)
贪心思想:对于不重叠的两区间,我们需要射一箭;对于重叠的两区间,我们应该结合起来,取两区间重叠的部分,再与下一个区间做比较。(可画图理解)-- 这里取两区间的重叠部分的实现办法(通过修改当前区间结束位置边界实现):由于已经排完序了,当前区间起始节点start就是重叠部分的start,直接使用,不做改变;而这时重叠部分的结束位置即是两区间结束点的较小值。(画图易理解)
代码如下:
class Solution {
public:
static bool cmp(const vector<int>& a, const vector<int>& b) { //按照气球的起始位置(a[0],b[0])排序,从前向后遍历气球数组
return a[0] < b[0];
}
int findMinArrowShots(vector<vector<int>>& points) {
int n = points.size();
if (n == 0) return 0;
sort(points.begin(), points.end(), cmp);
int ans = 1; //初始化箭的数量为1,因为points不为空至少需要一支箭
for (int i = 1; i < n; i++) {
if (points[i][0] > points[i - 1][1]) { //气球i和气球i-1不挨着(不重叠),注意不是>=
ans++; //需要一支箭
}
else { //气球i和气球i-1挨着(重叠)
points[i][1] = min(points[i - 1][1], points[i][1]); //更新当前气球的结束位置为两个气球结束位置中的较小值(因为一支箭的覆盖区间取的是相邻两个气球直径范围的重叠部分)
}
}
return ans;
}
};
题目二: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
题解:排序+贪心
这道题和刚刚那道题思路上差不多。
关键也是寻找重叠区间部分--即理解两区间重叠部分范围。当排完序后,两个区间相邻区间直接有重叠部分,意味着要除去一个区间--为了使移除数量最小,我们需要移除结束点end更大的区间。移除后,相当于将当前区间的end取较小值,后边再和之后的区间判断重叠,不断重复,直到遍历完。
代码如下:
class Solution {
public:
static bool cmp(const vector<int>& a, const vector<int>& b) {
return a[0] < b[0];
}
int eraseOverlapIntervals(vector<vector<int>>& intervals) {
int n = intervals.size();
if (n == 1) return 0;
int ans = 0;
sort(intervals.begin(), intervals.end(), cmp);
for (int i = 1; i < intervals.size(); i++) {
if (intervals[i][0] < intervals[i - 1][1]) { //重叠时(注意这里是< ,即恰好在端点相接不算重叠)
intervals[i][1] = min(intervals[i - 1][1], intervals[i][1]); //当前区间的end取重叠的两区间较小值--相当于将end大的那个区间移除
ans++;
}
}
return ans;
}
};
题目三:763.划分字母区间
题目链接
给你一个字符串
s
。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。注意,划分结果需要满足:将所有划分结果按顺序连接,得到的字符串仍然是
s
。返回一个表示每个字符串片段的长度的列表。
示例 1:输入:s = "ababcbacadefegdehijhklij" 输出:[9,7,8] 解释: 划分结果为 "ababcbaca"、"defegde"、"hijhklij" 。 每个字母最多出现在一个片段中。 像 "ababcbacadefegde", "hijhklij" 这样的划分是错误的,因为划分的片段数较少。示例 2:
输入:s = "eccbbbbdec" 输出:[10]提示:
1 <= s.length <= 500
s
仅由小写英文字母组成
题解:贪心+哈希+双指针
这道题也很经典:划分字母区间问题。
关键就在于:使用哈希存储每一个字符最后一次出现的位置,这样就好划分每一个区间的范围。
难点:双指针的使用:规定当前字母区间的左右边界并得出区间的长度 right - left + 1。左边界left容易得出,即每个边界的起点;右边界则是目前区间已出现的字符的最远边界(注意:右边界是在不断更新的边界。每次遍历都会引入新的元素,右边界代表着当前区间所出现的所有字母的最后一次出现的位置--直到当前遍历元素恰好大到达右边界的位置时,才结束这一字母区间划分)
代码如下:
class Solution {
public:
vector<int> partitionLabels(string s) {
int hash[27] = {0}; //i为字符(字符转化成的数字s[i] - 'a'),hash[i]为字符出现的最后位置
for (int i = 0; i < s.size(); i++) { //统计每一个字符最后出现的位置
hash[s[i] - 'a'] = i;
}
vector<int> ans;
int left = 0; //双指针
int right = 0;
for (int i = 0; i < s.size(); i++) {
right = max(right, hash[s[i] - 'a']); //找到目前已出现的字符的最远边界,更新右指针
if (i == right) { //当前遍历到的字符i即是目前已出现字符的最远边界时
ans.push_back(right - left + 1); //这一段的字符串长度
left = i + 1; //更新左指针,进入下一段区间
}
}
return ans;
}
};
三、小结
今天的打卡到此也就结束了,对于重叠区间问题也有了新的认识。最后,我是算法小白,但也希望终有所获。