参考
代码随想录
题目一:LeetCode 435.无重叠区间
怎么判断重叠
按照题目给出的示例,第一个区间的右边界与第二个区间的左边界重合不算重叠。对于区间问题,一般都要对区间进行排序,可以按照左边界或者右边界排序。按照个人习惯,将左边界较小的区间排在前,如果左边界相等,则右边界较小的排在前,因此对于某一个区间i和下一个区间i+1,当满足以下条件时,区间i+1和区间i重叠:
intervals[i][0] <= intervals[i+1][0] && intervals[i+1][0] < intervals[i][1]
特别注意,第一个不等式包含等号,第二个不包含等号。如果第二个等式也包含等号的话会把上图中的第二种情况也当作时重叠。如果预先对区间进行排列,保证左边界小的区间在前,那么上面的判断条件可以简化为:
intervals[i+1][0] < intervals[i][1]
怎么去重
题目中让求的是需要移除区间的最小数量,因此不需要真正的移除区间,而只要得到移除的区间个数就可以了。**如果两个有重叠,那么应该移除右边界较大的区间,保留右边界较小的区间。**因为当前区间的右边界越小,其占的区间就越小,留给下一个区间的空间就越大,重叠的可能性就越小(题目要求移除的区间数量要最小)。
代码怎么实现去重
在代码实现上不需要真正的移除区间,如果第i个区间和第i+1个区间有重叠,那么只需要将第i+1个区间的右边界更新为这两个区间右边界中较小者,这样就实现了去重操作,代码实现如下:
intervals[i+1][1] = min(intervals[i][1],intervals[i+1][1]);
经过上面的分析之后,整体的代码实现就不难了,代码如下:
class Solution {
public:
//左边界较小的排在前,如果左边界相等,则右边界较小的排在前
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 result = 0;
sort(intervals.begin(),intervals.end(),cmp);
for(int i = 0; i < intervals.size()-1; i++){
if(intervals[i+1][0] < intervals[i][1]){
intervals[i+1][1] = min(intervals[i][1],intervals[i+1][1]);
result++;
cout << i << endl;
}
}
return result;
}
};
题目二:LeetCode 763.划分字母区间
题目要求两个点,分割得到尽可能多的片段和每个字母只能出现在一个片段中。如果没有“每个字母尽可能出现在一个片段中”这个限制,那无非就是每个字符作为一个片段。每个字符只能出现在一个片段中,那么当前片段中如果出现了某个字符,那么这个片段的切割点至少包含到这个字符。
怎么保证切割得到尽可能多的片段
一个片段至少一个字符,如果该字符在字符串中的其他位置还存在,那么该切割片段至少包含到该字符出现的最后位置,例如,下图中第一切割片段的第一个字符为a,则这个片段至少包含到字符a出现的最后位置处('*'表示其他小写字母),说至少是因为其他字符可能还存在在更靠后的位置。这样目前找到了一个“最靠后”的切割点,以此类推,不断寻找从下一个字符开始到切割点之间字符的最后出现位置,以此来不断更新切割点。
在举个例子,如下图所示,第一个字符为a,找到字符a最后出现的位置为左图中分割线前的位置,此处作为当前的分割点,下一个字符为b,字符b最后出现的位置比当前的分割位置更靠后,因此要更新切割位置,如右下图所示,接着第三个字符为c,字符c最后出现的位置在当前切割位置的前面,因此不需要更新切割位置。这样,当遍历字符到切割位置时,说明当前片段中的字符在字符串出现的位置都已经包含在该片段中,因此可以作为一个切割片段了,这样切割就能得到尽可能多的片段。
怎么保证每个字符只出现在一个片段中
这个问题其实已经在上面说了,要找到该字符在字符串中最后出现的位置,这样就能保证每个字符只出现在一个片段中。
找到字符最后出现的位置
这里采用哈希的思想来记录字符最后出现的位置,即预先遍历一遍字符串,然后用数组记录每个字符最后出现的位置,这样可以大大提高效率。代码实现如下:
int lastPos[26];
for(int i = 0; i < s.size(); i++)
lastPos[s[i]-'a'] = i;
遍历一遍后,最后记录的就是每个字符最后出现的位置。
经过上面的分析之后,不难得到下面的完整代码:
class Solution {
public:
vector<int> partitionLabels(string s) {
vector<int> result;
int lastPos[26];
int start = 0, end = 0; //分割片段的起始位置和结束位置
for(int i = 0; i < s.size(); i++)
lastPos[s[i]-'a'] = i;
for(int i = 0; i < s.size(); i++){
end = max(end,lastPos[s[i]-'a']);
if(end == i){
result.push_back(end+1-start); //保存分割片段的长度
start = end + 1;
end = end + 1;
}
}
return result;
}
};
题目三:LeetCode 56.合并区间
还是一样的套路,先对区间进行排序,然后再判断相邻的两个区间有没有重叠部分。按照习惯,根据左边界的值从大到小进行排序,两个区间重叠的条件就是第二个区间的右边界小于等于第一个区间的右边界,注意这里的重叠的判断和“435.无重叠区间”这题有些不同。
重叠区间的合并
两个重叠区间的合并:左边界取第一个区间的左边界(注意这里已经经过排序,确保第一个区间的左边界小于等于第二个区间),右边界取两个区间中的较大者。
三个及以上重叠区间的合并:每次合并两个,多次合并即可
区间排序,区间重叠的判断等之前都已经做过。
整体代码实现如下:
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>> result;
sort(intervals.begin(),intervals.end(),cmp);
vector<int> tmp(2);
int index = 0;
while(index < intervals.size()){
tmp[0] = intervals[index][0];
tmp[1] = intervals[index][1];
index++;
while(index < intervals.size() && intervals[index][0] <= tmp[1]){ //合并多个区间(也可能不用合并)
tmp[1] = max(tmp[1],intervals[index][1]);
index++;
}
result.push_back(tmp);
}
return result;
}
};
上面的代码实现也不算太复杂,外层while循环遍历数组,里层while循环合并区间(也可能不用合并),用一个临时数组tmp来保存合并后的区间。看了代码随想录的代码后,将上面的代码进行优化,不使用tmp,也不需要两个while循环。初始化时将排序后的第一个区间直接加入结果数组中,然后从第一个区间开始合并,这样只需要判断当前的区间能不能和结果数组的最后一个区间合并,因为区间经过排序,能够确保相邻的区间才能够进行合并。优化后的代码实现如下:
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>> result;
sort(intervals.begin(),intervals.end(),cmp);
result.push_back(intervals[0]);
for(int i = 1; i < intervals.size(); i++){
if(intervals[i][0] <= result.back()[1])
result.back()[1] = max(result.back()[1],intervals[i][1]);
else
result.push_back(intervals[i]);
}
return result;
}
};