目录
LeetCode452.用最少数量的箭引爆气球
有一些球形气球贴在一堵用 XY 平面表示的墙面上。墙面上的气球记录在整数数组
points
,其中points[i] = [xstart, xend]
表示水平直径在xstart
和xend
之间的气球。你不知道气球的确切 y 坐标。一支弓箭可以沿着 x 轴从不同点 完全垂直 地射出。在坐标
x
处射出一支箭,若有一个气球的直径的开始和结束坐标为x
start
,x
end
, 且满足xstart ≤ x ≤ x
end
,则该气球会被 引爆 。可以射出的弓箭的数量 没有限制 。 弓箭一旦被射出之后,可以无限地前进。给你一个数组
points
,返回引爆所有气球所必须射出的 最小 弓箭数 。
思路:这里涉及到对重叠区间进行处理。
首先我们需要对这些气球排序,那是按照起始点坐标排序还是终止点坐标开始排序呢?
都可以的,但是选择过后注意如何更新。
1. 从前往后遍历
这里我们尝试按照起始点坐标进行排序,首先我们知道,如果说两个气球的区域重叠了,那么就可以使用一只箭将其一起引爆,而如果正好错开了,分割成两个区域,那么就需要另外一只箭了。
因此在这里我们从前往后开始遍历,当两个气球区域重叠的时候,那么就更新最新元素的终点坐标为两者中的最小值;而如果恰好没有重叠,那么箭的数量就需要加1。
static bool cmp(vector<int>& a, vector<int>& b){
return a[0] < b[0];//将气球按照出发点start从小到大排序
}
int findMinArrowShots(vector<vector<int>>& points) {
if(points.size() == 0) return 0;
sort(points.begin(), points.end(), cmp);//排序
int result = 1;//当point不为空时至少需要一只箭
for(int i = 1; i < points.size(); i ++){
if(points[i][0] > points[i - 1][1]){
//当下一个气球的左边起点大于了上一个气球的最小右边界,那么就说明需要另外一只箭了
result ++;
}else{
points[i][1] = min(points[i][1], points[i - 1][1]);//如果重合,那么就更新最小的右边界
}
}
return result;
}
时间复杂度:O(nlogn)
空间复杂度:O(1)
2. 从后往前遍历
当然也可以从后往前进行遍历,但是这时候是当重叠的时候将其更新为两者中起始点的最大值。
所以在选择遍历顺序的时候需要注意一下标准的选择。
static bool cmp(vector<int>& a, vector<int>& b){
return a[0] < b[0];//将气球按照出发点start从小到大排序
}
int findMinArrowShots(vector<vector<int>>& points) {
if(points.size() == 0) return 0;
sort(points.begin(), points.end(), cmp);//排序
int result = 1;//当point不为空时至少需要一只箭
for(int i = points.size() - 2; i >= 0; i --){
if(points[i][1] < points[i + 1][0]){
//当前一个气球的右边结束点小于了后一个气球的最大左边界点,那么就说明需要另外一只箭了
result ++;
}else{
points[i][0] = max(points[i][0], points[i + 1][0]);//如果重合,那么就更新最大的左边界
}
}
return result;
}
时间复杂度:O(nlogn)
空间复杂度:O(n)
LeetCode435.无重叠区间
给定一个区间的集合
intervals
,其中intervals[i] = [starti, endi]
。返回 需要移除区间的最小数量,使剩余区间互不重叠 。
思路:无重叠区间,起始和上面一道题很像,当然这里可以先计算未重叠区间个数,然后用总数将未重叠区间个数一减,就得到了需要删除的最小数量,当然也可以直接计算需要删除的区间个数,不同思路,殊途同归。
因此这里有多种方法可以做。
下面这个的思路是选择一种顺序进行排序,然后从前往后遍历找到重叠的区间个数,然后再从后往前去找重叠的区间个数,最后两者取最小值,即得到了最终需要删除的最小区间个数。但是需要注意一些细节,比如标志位下标的选择,否则没有办法合理地比较。
static bool cmp(vector<int>& a, vector<int>& b){
return a[0] < b[0];//按照元素的第一个值从小到大排序
}
int eraseOverlapIntervals(vector<vector<int>>& intervals) {
sort(intervals.begin(), intervals.end(), cmp);//排序
int count_pre = 0;//记录从前往后遍历时需要移除的数量
int index = 0;//设置从前往后遍历的初始标志位
int count_post = 0;//记录从后往前遍历时需要移除的数量
for(int i = 1; i < intervals.size(); i ++){
if(intervals[i][0] >= intervals[index][1]){
index = i;//更新标志位下标
continue;
}
//当后一个元素的起始点和前一个元素的终点位置重叠时需要删除该元素
count_pre ++;
}
index = intervals.size() - 1;//指向最后一个元素的下标,作为从后往前遍历时的初始标志位
for(int i = intervals.size() - 2; i >= 0; i --){
if(intervals[i][1] <= intervals[index][0]){
index = i;//更新标志位的下标
continue;
}
count_post ++;
}
int min_count = min(count_pre, count_post);//取两种遍历要删除元素的最小值
return min_count;
}
时间复杂度:O(nlogn)
空间复杂度:O(n)
下面这个的思路是将未交叉区间个数统计出来,最后用总数相减。
当然这里还用到了end来标记分割的位置,需要记录满足分割条件的区间个数,并且对于end要实时更新。
static bool cmp(vector<int>& a, vector<int>& b){
return a[1] < b[1];//按照元素的第二个值从小到大排序
}
int eraseOverlapIntervals(vector<vector<int>>& intervals) {
if(intervals.size() == 0) return 0;
sort(intervals.begin(), intervals.end(), cmp);//排序
int count = 1;//记录非交叉区间个数
int end = intervals[0][1];//记录区间的分割位置
for(int i = 1; i < intervals.size(); i ++){
if(end <= intervals[i][0]){//当后一个元素的起点大于等于了前面元素的终点时,说明这两个区间可以成为非交叉区间
//更新新的非交叉区间的结束位置
end = intervals[i][1];//非交叉区间的个数加1
count ++;
}
}
return intervals.size() - count;//总数减去非交叉个数即为需要删除的区间个数
}
当然也可以直接计算重叠区间的个数,满足不重叠区间的时候更新end,满足重叠区间的时候,取两者之间右边终点的最小值,然后重叠区间个数加1。
static bool cmp(vector<int>& a, vector<int>& b){
return a[0] < b[0];//按照元素的第一个值从小到大排序
}
int eraseOverlapIntervals(vector<vector<int>>& intervals) {
if(intervals.size() == 0) return 0;
sort(intervals.begin(), intervals.end(), cmp);//排序
int count = 0;//记录重叠区间个数
int end = intervals[0][1];//记录区间的分割位置
for(int i = 1; i < intervals.size(); i ++){
if(intervals[i][0] >= end){
end = intervals[i][1];//更新区间的分割位置
}else{
end = min(end, intervals[i][1]);//记录的一定是最小的右边界
count ++;
}
}
return count;//重叠区间个数即为需要删除的区间个数
}
上面代码可以进行简化,可以使用intervals[i][1]来代替end,也就是说跟射气球那道题的更新下标操作很像,这里只考虑重叠情况,首先重叠区间个数需要加1,同时需要更新intervals[i][1]为两者最小值。
static bool cmp(vector<int>& a, vector<int>& b){
return a[0] < b[0];//按照元素的第一个值从小到大排序
}
int eraseOverlapIntervals(vector<vector<int>>& intervals) {
if(intervals.size() == 0) return 0;
sort(intervals.begin(), intervals.end(), cmp);//排序
int count = 0;//记录重叠区间个数
for(int i = 1; i < intervals.size(); i ++){//只记录重叠情况
if(intervals[i][0] < intervals[i - 1][1]){
intervals[i][1] = min(intervals[i][1], intervals[i - 1][1]);
count ++;
}
}
return count;//重叠区间个数即为需要删除的区间个数
}
其实气球那道题的代码稍微改一下,就可以适用于本题。
所要求的箭的数量其实就是非重叠区间的个数,有多少只箭,就有多少个非重叠区间,总数相减即得到了最小需要删除的区间个数。
static bool cmp(vector<int>& a, vector<int>& b){
return a[0] < b[0];//按照元素的第一个值从小到大排序
}
int eraseOverlapIntervals(vector<vector<int>>& intervals) {
if(intervals.size() == 0) return 0;
sort(intervals.begin(), intervals.end(), cmp);//排序
int count = 1;//记录非重叠区间
for(int i = 1; i < intervals.size(); i ++){
if(intervals[i][0] >= intervals[i - 1][1]){
count ++;
}else{
intervals[i][1] = min(intervals[i][1], intervals[i - 1][1]);
}
}
return intervals.size() - count;
}
不管是按照起始点大小排序还是终止点排序,最后结果都是一样的。
static bool cmp(vector<int>& a, vector<int>& b){
return a[1] < b[1];//按照元素的第二个值从小到大排序
}
int eraseOverlapIntervals(vector<vector<int>>& intervals) {
if(intervals.size() == 0) return 0;
sort(intervals.begin(), intervals.end(), cmp);//排序
int count = 1;//记录非重叠区间
for(int i = 1; i < intervals.size(); i ++){
if(intervals[i][0] >= intervals[i - 1][1]){
count ++;
}else{
intervals[i][1] = min(intervals[i][1], intervals[i - 1][1]);
}
}
return intervals.size() - count;
}
时间复杂度:O(nlogn)
空间复杂度:O(n)
LeetCode73.划分字母区间
给你一个字符串
s
。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。注意,划分结果需要满足:将所有划分结果按顺序连接,得到的字符串仍然是
s
。返回一个表示每个字符串片段的长度的列表。
思路:如果第一次遇到这种题,可能会十分局促,有种从哪里都不太好入手的感觉。
我怎么知道哪里应该截断?万一从这里截断并不合理,后面右开始出现了相关元素怎么办?
所以这里介绍一种思路,那就是记录下在字符串中出现元素的最大下标值。
循环遍历过程中,不断比较获取最大下标值,当当前下标与最大下标值相等的时候,这个时候就可以截断了,因为从从这里往前的元素的最后一次出现都不会超过这个下标,从这里阶段是合理的。
及时更新左边起始下标,后面这样不断循环,最后就能得到答案。
vector<int> partitionLabels(string s) {
int records[27] = {0};//辅助数组,记录元素最后出现的位置
for(int i = 0; i < s.size(); i ++){
records[s[i] - 'a'] = i;//在记录过程中会不断更新,最后得到的就是元素最后出现的位置
}
vector<int> result;//收集长度
int right = 0;//记录元素下标的最大值
int left = 0;
for(int i = 0; i < s.size(); i ++){
right = max(right, records[s[i] - 'a']);//记录下标最大值
if(i == right){//当索引i与right相等时,说明找到了某个元素最后出现的位置,这个时候可以进行一次处理
result.push_back(right - left + 1);
left = i + 1;//更新left
}
}
return result;
}
时间复杂度:O(n)
空间复杂度:O(1)
当然这里有另外一种思路。因为我们前面两道题都是讲的区间,其实这里就可以将各元素出现的起始坐标和最后出现的最大坐标保存下来,截断的触发条件也就是前一个区间的终点小于后一个区间的起点,这个时候就是截断的时机,当然,为了保证是有序的,还是需要先进行排序工作,才能进行后续截断。
这里注意,如果说最后一段存在的话,在跳出循环后需要将最后一段加入结果数组中,因为对于区间里面的最后一段,即使满足题意,在数组区间里面也找不到能够将其截断的条件了,也就是说没办法找到一个区间的起始下标能够大于size()-1,这本身就是最后一个元素了,因此在跳出循环后需要额外将其加入。
static bool cmp(vector<int>& a, vector<int>& b){
return a[0] < b[0];//按照第一个元素从小到大进行排列
}
vector<vector<int>> countAlp(string& s){
vector<vector<int>> res;//记录最终结果
vector<vector<int>> records(27, vector<int>(2, INT_MIN));//记录各元素的区间范围
for(int i = 0; i < s.size(); i ++){//将各元素的起始位置以及最终出现的位置记录下来,形成一个区间
if(records[s[i] - 'a'][0] == INT_MIN){
records[s[i] - 'a'][0] = i;
}
records[s[i] - 'a'][1] = i;
}
for(int i = 0; i < 27; i ++){//将所有出现过的元素统计出来,加入res,没出现过的就跳过
if(records[i][0] != INT_MIN){
res.push_back(records[i]);
}
}
return res;
}
vector<int> partitionLabels(string s) {
vector<int> result;
vector<vector<int>> count = countAlp(s);//获得元素出现区间
sort(count.begin(), count.end(), cmp);//排序
int right = count[0][1];//初始化为第一个元素的右边终点值
int left = 0;//从下标0开始
for(int i = 1; i < count.size(); i ++){
if(right < count[i][0]){//当出现一个元素的起始位置大于right时,可以开始进行分割
result.push_back(right - left + 1);
left = count[i][0];
}
right = max(right, count[i][1]);
}
result.push_back(right - left + 1);//还有最后一段需要进行分割
return result;
}
时间复杂度:O(n)
空间复杂度:O(n)
感谢你的阅读,希望我的文章能够给你帮助,如果有帮助,麻烦点赞加收藏,或者点点关注,非常感谢。
如果有什么问题欢迎评论区讨论!