视频讲解:
贪心算法,依然是判断重叠区间 | LeetCode:435.无重叠区间_哔哩哔哩_bilibili
453. 无重叠区间
思路:本题的要求是 返回需要移除区间的最小数量,使剩余区间互不重叠,打好这道题可以使我们明确如何界定两个区间存在重叠,但是本题特殊的地方在于他不涉及重叠区间的更新操作。像前一天所做的射气球的问题,需要不断取重叠区间的交集部分用来表示射箭的区域,但是本题要求的是删去重叠区间中的一者使得区间彼此独立。
显然区间的比较是一个两两的比较关系,同时我们将区间按照下限或者上限递增排序之后,从左向右进行遍历,还可以保证当前遍历的位置无需考虑是否存在其左侧不会存在重叠的区间。这就是从左遍历下限最小的区间所产生的效果。我们以a代指当前遍历位置,b表示后一个位置,看似是比较a与b是否重合就可以满足局部题解,重叠集合之后更新的策略开始时我认为是保存跨越距离最小的集合就可以,但是这种情况推不出最优题解。应该是联合考虑b后一个的c位置,优化局部题解操作,使之通过不断积累,得到最优题解。我们看a,b,c三者的关系有这样几种,
- a∩b,c∩b,c不∩a, delete b;
- a∩b,c∩b,c∩a, delete b,c;
- a∩b,c不∩b, delete b;
- a∩b,c不∩b,c∩a, 不可能;
- a不∩b,c与b关系不需要明确, 指向b继续遍历;
回看这里所有的情况,其中最为关键的就是 a∩b,c∩b,c不∩a 这种,如果按照之前保存距离的方式,会导致a被删,c也被删,删的次数不满足贪心找最少的删除次数,这也就是导致我无法得到最优解的原因,所以集合的更新条件是,对于存在交集的集合,保留上限更小的集合,删除上限大的;上限越小与之后集合重合的概率越低,这就是我解题的贪心策略。
// 时间复杂度O(nlogn) 排序耗时更大
// 空间复杂度O(1)
class Solution {
public int eraseOverlapIntervals(int[][] intervals) {
// 递增排列排列
Arrays.sort(intervals, (a,b)->{
if(a[0] < b[0]) return -1;
if(a[0] == b[0]) return 0;
// if(a[1] < b[1]) return -1;
// else if(a[1] == b[1]) return 0;
// else return 1;
return 1;
});
int ans = 0;
int[] cur = intervals[0];
for(int i=1; i<intervals.length; i++){
// 不存在重叠,那么当前区间是可以留下来的,cur自动移动下个区间
if(intervals[i][0] >= cur[1])
cur = intervals[i];
else{
if(cur[1] > intervals[i][1])
cur = intervals[i];
ans++;
}
}
// 优化一下
// int ans = 0;
// int cur = intervals[0][1];
// for(int i=1; i<intervals.length; i++){
// // 不存在重叠,那么当前区间是可以留下来的,cur自动移动下个区间
// if(intervals[i][0] >= cur)
// cur = intervals[i][1];
// else{
// if(cur > intervals[i][1])
// cur = intervals[i][1];
// ans++;
// }
// }
return ans;
}
}
优化后时间效率与原来的解法差不多。
763.划分字母区间
思路:本题可以理解为获取到每个字母出现的范围区间,然后输出不存在重叠的区间的区间长度。所以在不断遍历字符串s的过程中记录各个字母出现的最大右位置作为全局上限,如果当前i与全局全局上限相等,那说明i之前的元素可以被分割在一个片段之内。
全局上限储存了其中出现于最为右端的字母的索引下标,其他的字母的最后一次出现位置都小于他,因此可以将这个片段分割出来,然后继续访问字符串剩余元素开始继续遍历。
// 时间复杂度O(n^2),因为lastIndexOf时间开销为O(n)
// 空间复杂度O(n)
class Solution {
public List<Integer> partitionLabels(String s) {
// 重叠区间的关键求解思路就是不断遍历新的范围,不会更新最大右上限,如果出现下限都比当前最大上限来的大则说明不重叠,或者类似出现了可以分割的便捷位置。
int splitIndex = -1;
int preSplit = 0;
List<Integer> ans = new ArrayList<>();
for (int i=0; i<s.length(); i++) {
char ch = s.charAt(i);
// 计算每个字母的最长有边界
if(s.lastIndexOf(ch) > splitIndex)
splitIndex = s.lastIndexOf(ch);
if(i == splitIndex){
ans.add(splitIndex - preSplit + 1);
preSplit = splitIndex+1; // 怕pre会地址越界,那么此时split是最后一个元素,那么i也是,那么马上for循环就结束了
}
}
return ans;
}
}
435. 合并区间
思路:在射箭以及无重叠区间所形成的先排序再操作重叠区间的思路之下,本题于我个人而言是今天三题中最简单的一题,区间重合时的更新策略也非常的清晰,就是形成并集,对于不重合的区间,就将指针移动到区间位于数轴更为右侧的那个区间即可。
// 时间复杂度O(nlogn)
// 空间复杂度 小于等于O(n)
class Solution {
public int[][] merge(int[][] intervals) {
// 优先排序,防止出现一个非常偏远的范围出现在密集的范围之中引发问题
Arrays.sort(intervals, (a,b)->{
return a[0]-b[0];
});
List<int[]> list = new ArrayList<>();
int[] cur = intervals[0];
for(int i=1; i<intervals.length; i++){
// 存在重叠
if(intervals[i][0] <= cur[1]){
cur[0] = Math.min(intervals[i][0], cur[0]);
cur[1] = Math.max(intervals[i][1], cur[1]);
}else{
list.add(cur);
cur = intervals[i];
}
}
list.add(cur);
int[][] res = new int[list.size()][2];
for(int i=0; i<list.size(); i++)
res[i] = list.get(i);
return res;
}
}
一点小总结:
针对重叠区间类型的题目,自己形成的惯用解法就是先排序,然后按照是否重合的操作对每个元素进行整合,可附带构建额外的空间存储已经独立的空间。另外如果不断重叠空间有合并的需求,那么在进行遍历时,只需要不断更新所访问到的最大上限,利用上限值与新的位置的下限值进行比较,判断是否存在重叠,然后更新最大上限,将区间进一步向后推,也符合排序之后从左向右进行访问的过程。
- 需要合并区间,则排序+当前指针指向元素更新+额外空间存储独立可行集合;
- 不需要合并区间,则排序+保存最大上限;
- 不需要合并空间,并且是类似于字符串分割这样的自动形成的有序区间,则直接更新并保存最大上限求解即可。