1、非交叉区间的最大个数/交叉区间最小个数
1.1 leetcode 435:无重叠区间
第一遍代码的思路错了
第一遍代码思路:按每个集合的开始元素从小到大排序,对于有相同的开始元素的集合,把集合大小 小的排在前面。因为集合大小更小的集合更不容易跟后面产生重叠
对于后面加进来的元素,通过判断起始位置和之前终点的位置关系,如果不重合更新终点位置,重合那么计数的变量**+1**,其他啥也不干,相当于删除
latestEnd 初始化的时候应该是比能输入的最小值还小
class Solution {
public:
//按每个集合的开始元素从小到大排序,对于有相同的开始元素的集合,把集合大小小的排在前面
//因为集合大小更小的集合更不容易跟后面产生重叠,对于后面加进来的元素,通过判断起始位置
//和之前终点的位置关系,如果不重合更新终点位置,重合那么计数的变量+1,其他啥也不干,相当于删除
static bool cmp(vector<int>& a, vector<int>& b) {
if(a[0] == b[0]) return a[1] < b[1];
return a[0] < b[0];
}
int eraseOverlapIntervals(vector<vector<int>>& intervals) {
int res = 0;
int latestEnd = -50001;//初始化的时候应该是比能输入的最小值还小
sort(intervals.begin(), intervals.end(), cmp);
for(int i = 0; i < intervals.size(); i++) {
if(intervals[i][0] >= latestEnd) {
latestEnd = intervals[i][1];
}
else {
res++;
}
}
return res;
}
};
这个逻辑是错的,看这个例子
[[-52,31],[-73,-26],[82,97],[-65,-11],[-62,-49],[95,99],[58,95],[-31,49],[66,98],[-63,2],[30,47],[-40,-26]]
通过第一遍代码整出来的结果是8,但是实际结果是7
为什么呢?因为当多个数组出现重叠的时候,没有考虑清楚需要保留哪一个。上面的第一遍代码默认谁先遍历到就保留谁,也就是左边界最小的那个,但是显然是不对的
要保留的那个数组需要 最小可能性与后面的数组产生重合(找最少重叠数组),那么应该找一堆重叠数组里面右边界最小的那个,他才是最不可能与后面的数组产生重合的那个,也就是一堆重合数组中应该保留的
以上面的那个例子为例,进行排序后取前几个:
[-73, -26], [-65, -11], [-63, 2], [-62, -49], [-52, 31]
按第一遍代码,除了保留第一个[-73, -26]外全给删了,但是如果按改进思路,对于前四个重叠的数组应该保留[-62, -49],那么第五个数组[-52, 31]就不重合了,就不用删了
class Solution {
private:
static bool cmp(vector<int> &a, vector<int> &b) {
return a[0] < b[0];
}
public:
int eraseOverlapIntervals(vector<vector<int>>& intervals) {
sort(intervals.begin(), intervals.end(), cmp);
int finalEnd = intervals[0][1];
int count = 0;
for (int i = 1; i < intervals.size(); i++) {
if (intervals[i][0] < finalEnd) count++;
else finalEnd = intervals[i][1]; // 如果没有重叠,一定要更新右边界(注意)
if (intervals[i][1] < finalEnd) finalEnd = intervals[i][1]; // 在重叠的情况下,只更新小的边界
}
return count;
}
};
根据改进思路,在重叠的情况下找里面右边界最小的那个,即每找到一个重叠的,就看看是否右边界更小,更小就更新。改完后ac
逻辑更清晰的版本
class Solution {
public:
//按每个集合的开始元素从小到大排序,对于有相同的开始元素的集合,把集合大小小的排在前面
//因为集合大小更小的集合更不容易跟后面产生重叠,对于后面加进来的元素,通过判断起始位置
//和之前终点的位置关系,如果不重合更新终点位置,重合那么计数的变量+1,其他啥也不干,相当于删除
static bool cmp(vector<int>& a, vector<int>& b) {
if(a[0] == b[0]) return a[1] < b[1];
return a[0] < b[0];
}
int eraseOverlapIntervals(vector<vector<int>>& intervals) {
int res = 0;
int latestEnd = -50001;//初始化的时候应该是比能输入的最小值还小
sort(intervals.begin(), intervals.end(), cmp);
for(int i = 0; i < intervals.size(); i++) {
if(intervals[i][0] >= latestEnd) {
latestEnd = intervals[i][1];
}
else {
res++;
latestEnd = min(latestEnd, intervals[i][1]);//找一堆重叠数组里面右边界最小
}
}
return res;
}
};
代码随想录对这一思路的实现:
左边界排序我们就是直接求 重叠的区间,count为记录重叠区间数
对于有相同的开始元素的集合,不需要把集合大小 小的排在前面,这样做也是没有道理的,并不是说相同的最小开始元素的集合里面挑 集合大小小 的就是一堆重叠数组里面留下来的,而是右边界最靠前的
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) {
if (intervals.size() == 0) return 0;
sort(intervals.begin(), intervals.end(), cmp);
int count = 0; // 注意这里从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变量,只判断 重叠情况就好
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) {
if (intervals.size() == 0) return 0;
sort(intervals.begin(), intervals.end(), cmp);
int count = 0; // 注意这里从0开始,因为是记录重叠区间
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]);
count++;
}
}
return count;
}
};
思路
相信很多同学看到这道题目都冥冥之中感觉要排序,但是究竟是按照右边界排序,还是按照左边界排序呢?
其实都可以
右边界适合判断非重叠(非交叉),左边界适合判断重叠(交叉)
主要就是为了让区间尽可能的重叠,我来按照右边界排序,从左向右统计非交叉区间的个数(取到最大值,想要非交叉的区间最多,显然应该关注右边界)。最后用区间总数 减去 非交叉区间的个数的最大值 就是需要移除的区间个数了
此时问题就是要求 非交叉区间的 最大个数,这里记录非交叉区间的个数还是有技巧的,如图:
区间1,2,3,4,5,6都按照右边界排好序
当确定区间 1 和 区间2 重叠后,如何确定是否与 区间3 也重叠呢?
就是取 区间1 和 区间2 右边界的最小值,因为这个最小值之前的部分一定是 区间1 和区间2 的重合部分,如果这个最小值也触达到区间3,那么说明 区间 1,2,3都是重合的,如果大于这个最小值就不和1重合了,最后留下的非重叠区间也是1(即右边界最小的那个,即最先遍历到的)
接下来就是找大于区间1结束位置的区间,是从区间4开始。区间4结束之后,再找到区间6,所以一共记录非交叉区间的个数是三个
总共区间个数为6,减去非交叉区间(1,4,6三个区间)的个数的最大值3。移除区间的最小数量就是3
与第一遍代码不同,他这个记录的是非交叉的区间数的最大值,而不是需要删除的区间个数。C++代码如下:
class Solution {
public:
// 按照区间右边界排序
static bool cmp (const vector<int>& a, const 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];
count++;
}
}
return intervals.size() - count;
}
};
时间复杂度:O(nlog n) ,有一个快排
空间复杂度:O(n),有一个快排,最差情况(倒序)时,需要n次递归调用。因此确实需要O(n)的栈空间
本题其实和leetcode 452:用最少数量的箭引爆气球非常像,弓箭的数量就相当于是非交叉区间的数量,只要把弓箭那道题目代码里射爆气球的判断条件加个等号(认为[0,1][1,2]不是相邻区间),然后用总区间数减去弓箭数量 就是要移除的区间数量了
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) {
if (intervals.size() == 0) return 0;
sort(intervals.begin(), intervals.end(), cmp);
int result = 1; // points 不为空至少需要一支箭
for (int i = 1; i < intervals.size(); i++) {
if (intervals[i][0] >= intervals[i - 1][1]) {
result++; // 需要一支箭
}
else { // 气球i和气球i-1挨着
intervals[i][1] = min(intervals[i - 1][1], intervals[i][1]); // 更新重叠气球最小右边界
}
}
return intervals.size() - result;
}
};
2、找重叠区间的起始 / 结束位置
2.1 leetcode 763:划分字母区间
直接的思路:第一个字符是一定加入的,只要后面出现了跟第一个字符相同的元素,中间所有元素都属于第一个区间,后面出现这些元素 也需要划为一段
class Solution {
public:
vector<int> partitionLabels(string s) {
unordered_set<char> myset;
int i = 0;
vector<int> res;
myset.insert(s[i]); // 每次都要把第一个元素塞进去
while (i < s.size()) {
int start = i; // start是起点,i是不断更新的终点+1,下一段的起点
i++; // 下一段的起点至少是start+1
for (int j = i; j < s.size(); j++) {
if (myset.find(s[j]) != myset.end()) {
for (int ii = i; ii < j; ii++) { // 之前已经出现过的元素肯定在一个集合中,所以要把之前的元素都加入集合
myset.insert(s[ii]);
}
i = j + 1; // 新的终点+1, 下一段的起点
}
}
myset.clear();
myset.insert(s[i]);// 每次都要把第一个元素塞进去
res.push_back(i - start); // i本来就是后一个元素,所以不用+1
}
return res;
}
};
其实思路跟直接的思路一致
记录每个字母第一次出现以及最后一次出现的位置,就转成重叠区间问题了
要划分为尽可能多的片段,就可以转换为求重叠区间(左边界排序) 的开始和结束位置
跟上一题不同,重叠区间内部 选择 终止位置的最大值作为重叠区间的结束位置,而且初始化,包括取得阶段结果后的初始化都有区别
具体转化方式以及求起始位置和终止位置的方法见注释
class Solution {
public:
//记录每个字母第一次出现以及最后一次出现的位置,就转成重叠区间问题了
//要划分为尽可能多的片段,就可以转换为求重叠区间(左边界排序) 最大的个数
//跟上一题不同,重叠区间内部选择终止位置的最大值
static bool cmp(vector<int>& a, vector<int>& b) {
return a[0] < b[0];
}
vector<int> partitionLabels(string s) {
vector<bool> isJudged(s.size(), false);//判断该位置上的字母是否被判断过了
vector<vector<int>> qujian;//存储每个字母第一次出现和最后一次位置的数组
for(int i = 0; i < s.size(); i++) {
if(isJudged[i] == true) continue;
char c = s[i];
int start = i;
int end = i;//左闭右闭区间
for(int j = i; j < s.size(); j++) {
if(s[j] == c) {
isJudged[j] = true;
end = j;
}
}
vector<int> tmp(2, 0);
tmp[0] = start+1;
tmp[1] = end+1;
qujian.push_back(tmp);
}
// for(int i = 0; i < qujian.size(); i++) {
// cout << s[qujian[i][0]] << " " << qujian[i][0] << " " << qujian[i][1] << endl;
// }
//每个字母第一次出现和最后一次位置的数组构造完毕,接下来就是找重叠区间的起始和结束位置
//因为这个重叠区间的结尾位置与上一题不同,应该是所有区间里面结尾位置的最大值
int end = qujian[0][1];
//因为需要跟终点位置比,所以要初始化为第一个数组的终点位置
int start = qujian[0][0];
vector<int> res;
sort(qujian.begin(), qujian.end(), cmp);
for(int i = 1; i < qujian.size(); i++) {
if(qujian[i][0] <= end) {
end = max(end, qujian[i][1]);
}
else {
res.push_back(end - start + 1);
start = qujian[i][0];
end = qujian[i][1];
//因为需要跟终点位置比,所以要及时更新下一个重叠数组的终点位置
}
}
res.push_back(end - start + 1);
return res;
}
};
代码随想录的相同思路的相似实现
提供一种与leetcode 452:用最少数量的箭引爆气球、leetcode 435:无重叠区间 相似的解法
统计字符串中所有字符的起始和结束位置,记录这些区间(实际上也就是leetcode 435:无重叠区间 题目里的输入),将区间按左边界从小到大排序,找到边界将区间划分成组,互不重叠。找到的边界就是答案
主要跟第一遍代码的区别是:记录每个字母出现的区间部分使用hash表实现,因为一共就26个字母,即 countLabels() 函数的部分
class Solution {
public:
static bool cmp(vector<int> &a, vector<int> &b) {
return a[0] < b[0];
}
// 记录每个字母出现的区间
vector<vector<int>> countLabels(string s) {
vector<vector<int>> hash(26, vector<int>(2, INT_MIN));
vector<vector<int>> hash_filter;
for (int i = 0; i < s.size(); ++i) {
if (hash[s[i] - 'a'][0] == INT_MIN) {
hash[s[i] - 'a'][0] = i;
}
hash[s[i] - 'a'][1] = i;
}
// 去除字符串中未出现的字母所占用区间
for (int i = 0; i < hash.size(); ++i) {
if (hash[i][0] != INT_MIN) {
hash_filter.push_back(hash[i]);
}
}
return hash_filter;
}
vector<int> partitionLabels(string s) {
vector<int> res;
// 这一步得到的 hash 即为无重叠区间题意中的输入样例格式:区间列表
// 只不过现在我们要求的是区间分割点
vector<vector<int>> hash = countLabels(s);
// 按照左边界从小到大排序
sort(hash.begin(), hash.end(), cmp);
// 记录最大右边界
int rightBoard = hash[0][1];
int leftBoard = 0;
for (int i = 1; i < hash.size(); ++i) {
// 由于字符串一定能分割,因此,
// 一旦下一区间左边界大于当前右边界,即可认为出现分割点
if (hash[i][0] > rightBoard) {
res.push_back(rightBoard - leftBoard + 1);
leftBoard = hash[i][0];
}
rightBoard = max(rightBoard, hash[i][1]);
}
// 最右端
res.push_back(rightBoard - leftBoard + 1);
return res;
}
};
思路
一想到分割字符串就想到了回溯,但本题其实不用回溯去暴力搜索
题目要求同一字母最多出现在一个片段中,那么如何把同一个字母的都圈在同一个区间里呢?
在遍历的过程中相当于是要找每一个字母的边界,如果找到之前遍历过的所有字母的最远边界,说明这个边界就是分割点了。此时前面出现过所有字母,最远也就到这个边界了(思路与第一遍代码一致)
可以分为如下两步:
1、统计每一个字符最后出现的位置
2、从头遍历字符,并更新字符的最远出现下标,如果找到字符最远出现位置下标 和 当前下标 相等了,则找到了分割点
代码随想录代码中 统计每一个字符最后出现的位置 还是很巧的
class Solution {
public:
vector<int> partitionLabels(string S) {
int hash[27] = {0}; // i为字符,hash[i]为字符出现的最后位置
for (int i = 0; i < S.size(); i++) { // 统计每一个字符最后出现的位置
hash[S[i] - 'a'] = i;
}
vector<int> result;
int left = 0;
int right = 0;
for (int i = 0; i < S.size(); i++) {
right = max(right, hash[S[i] - 'a']); // 找到字符出现的最远边界
if (i == right) {
result.push_back(right - left + 1);
left = i + 1;
}
}
return result;
}
};
时间复杂度:O(n)
空间复杂度:O(1),使用的hash数组是固定大小
2.2 leetcode 763:总结
思路部分用最远出现距离模拟了圈字符的行为,之前也使用了跟上题相似的格式,但是具体到想法 对于右边界的选择 不一样
3、合并重叠区间
3.1 leetcode 56:合并区间
第一遍代码
跟上题 第一遍代码后半部分找重叠区间的起始和结束位置 思路完全一致
注意排序之后再确定start和end的初值,不然下面例子就不对了
Testcase
[[1,4],[0,4]]
Answer
[[0,4]]
class Solution {
public:
//跟上题 第一遍代码后半部分找重叠区间的起始和结束位置 思路完全一致
static bool cmp(vector<int>& a, vector<int>& b) {
return a[0] < b[0];
}
vector<vector<int>> merge(vector<vector<int>>& intervals) {
vector<vector<int>> res;
sort(intervals.begin(), intervals.end(), cmp);
int end = intervals[0][1];
int start = intervals[0][0];
/*
注意排序之后再确定start和end的初值,不然下面例子就不对了
Testcase
[[1,4],[0,4]]
Answer
[[0,4]]
*/
for(int i = 1; i < intervals.size(); i++) {
if(intervals[i][0] <= end) {
end = max(end, intervals[i][1]);
}
else {
vector<int> tmp;
tmp.push_back(start);
tmp.push_back(end);
res.push_back(tmp);
start = intervals[i][0];
end = intervals[i][1];
}
}
vector<int> tmp;
tmp.push_back(start);
tmp.push_back(end);
res.push_back(tmp);
return res;
}
};
思路
本题的本质其实还是判断重叠区间问题
与leetcode 452:用最少数量的箭引爆气球 和 leetcode 435:无重叠区间 都是一个套路
这几道题都是判断区间重叠,区别就是判断区间重叠后的逻辑,本题是判断区间重叠后要进行区间合并
所以一样的套路,先排序,让所有的相邻区间尽可能的重叠在一起,按左边界,或者右边界排序都可以,处理逻辑稍有不同
按照左边界从小到大排序之后,如果 intervals[i][0] <= intervals[i - 1][1]
即intervals[i]的左边界 <= intervals[i - 1]的右边界,则一定有重叠。(本题相邻区间也算重叠,所以是**<=**)
这么说有点抽象,看图:(注意图中区间都是按照左边界排序之后了)
知道如何判断重复之后,剩下的就是合并了,如何去模拟合并区间呢?
其实就是用合并区间后左边界和右边界,作为一个新的区间,加入到result数组里就可以了。如果没有合并就把原区间加入到result数组
class Solution {
private:
static bool cmp(vector<int> &a, vector<int> &b) {
return a[0] < b[0];
}
public:
vector<vector<int>> merge(vector<vector<int>>& intervals) {
vector<vector<int>> res;
sort(intervals.begin(), intervals.end(), cmp);
int curEnd = intervals[0][1];
int curStart = intervals[0][0];
for (int i = 1; i < intervals.size(); i++) {
if (intervals[i][0] <= curEnd) // =也算重合
curEnd = max(curEnd, intervals[i][1]);
else {
vector<int> tmp = {curStart, curEnd};
res.push_back(tmp);
curStart = intervals[i][0];
curEnd = intervals[i][1];
}
}
res.push_back(vector<int>({curStart, curEnd})); // 最后还有一个别忘了塞进去
return res;
}
};
代码随想录本题的写法骚的,直接把初始结果放在result里面,修改时 直接改result里面的数据
C++代码如下:
class Solution {
public:
vector<vector<int>> merge(vector<vector<int>>& intervals) {
vector<vector<int>> result;
if (intervals.size() == 0) return result; // 区间集合为空直接返回
// 排序的参数使用了lambda表达式
sort(intervals.begin(), intervals.end(), [](const vector<int>& a, const vector<int>& b){return a[0] < b[0];});
// 第一个区间就可以放进结果集里,后面如果重叠,在result上直接合并
result.push_back(intervals[0]);
for (int i = 1; i < intervals.size(); i++) {
if (result.back()[1] >= intervals[i][0]) { // 发现重叠区间
// 合并区间,只更新右边界就好,因为result.back()的左边界一定是最小值,因为我们按照左边界排序的
result.back()[1] = max(result.back()[1], intervals[i][1]);
} else {
result.push_back(intervals[i]); // 区间不重叠
}
}
return result;
}
};
时间复杂度: O(nlogn)
空间复杂度: O(logn),排序需要的空间开销