前言
本节内容为贪心算法中,关于重叠区间的问题,其实与贪心没多大关系,解题思路都比较靠技巧。
无重叠区间
题目详细:LeetCode.435
这道题和上一节的《LeetCode.452. 用最少数量的箭引爆气球》还蛮相似的,同样是对重叠区间进行判断和计算,思路都写在代码注释里了,详细的题解可查阅:《代码随想录》— 无重叠区间
Java解法:
class Solution {
public int eraseOverlapIntervals(int[][] intervals) {
// 按左边界从小到大排序
Arrays.sort(intervals, (a, b) -> {
return a[0] - b[0];
});
// 非交叉区间的个数,移除多个重叠区间,最后至少存在一个
int count = 1;
for(int i = 1; i < intervals.length; i++){
if(intervals[i - 1][1] > intervals[i][0]){
// 上一个区间的右边界 > 当前区间的左边界,说明两个区间是相交的
// 保留一个两者之间最小的右边界,即相交的右边界
intervals[i][1] = Math.min(intervals[i - 1][1], intervals[i][1]);
}else{
// 上一个区间相交的最大右边界 <= 当前区间的左边界
// 则说明当前区间与前面的区间都不相交,非交叉区间个数 + 1
count++;
}
}
// 最后利用(区间总数 - 非交叉区间的个数) = 移除区间的最小数量
return intervals.length - count;
}
}
划分字母区间
题目详细:LeetCode.763
根据前两个重叠区间问题的解题思路,在这道题中我直接尝试利用重叠区间来解题,解题思路如下:
- 遍历字符串,利用二维数组映射字符串中每个字符的区间,这里的区间下标我从1开始记录,主要是方便后面跳过空区间
- 接着,以区间的左边界对二维数组从小到大进行排序
- 因为有些字母在字符串中可能没有出现,所以需要跳过空区间,即跳过区间大小为
[0, 0]
的区间 - 定义一个变量 left,用于记录重叠区间起始的下标位置
- 循环比较当前区间的左边界和上一个区间的右边界
- 如果
上一个区间的右边界 > 当前区间的左边界
,则说明这两个区间是重叠的,让当前区间的右边界保留一个最大的右边界值 - 否则,说明两个区间不是重叠的,那么后续的区间中肯定不会出现重复的字母,所以产生一个划分点,注意划分后要更新变量 left
- 如果
- 当遍历到最后一个区间,最后的一个划分点刚好在数组尾部,但此时已结束循环,所以最后需要再补充添加最后一段重叠区间的划分长度。
Java解法(重叠区间):
class Solution {
public int[][] strToLetterRangeArr(String s){
// 利用二维数组映射字符串中每个字符的区间
int[][] letter_ranges = new int[26][2];
for(int i = 0; i < s.length(); i++){
int l_index = s.charAt(i) - 'a';
int[] range = letter_ranges[l_index];
if(range[0] == 0){
range[0] = i + 1;
range[1] = i + 1;
}else{
range[1] = i + 1;
}
}
// 以区间的左边界对区间从小到大排序
Arrays.sort(letter_ranges, (a, b) -> {
return a[0] - b[0];
});
int i = 0;
// 跳过未出现字母的空区间
while(i < letter_ranges.length){
if(letter_ranges[i][0] != 0){
break;
}
i++;
}
return Arrays.copyOfRange(letter_ranges, i, letter_ranges.length);
}
public List<Integer> partitionLabels(String s) {
List<Integer> ans = new ArrayList<>();
// 利用二维数组映射字符串中每个字符的区间,并以区间的左边界对区间从小到大排序
int[][] letter_ranges = this.strToLetterRangeArr(s);
// left确定了重叠区间起始的下标位置
int i = 1, left = letter_ranges[0][0];
for(; i < letter_ranges.length; i++){
if(letter_ranges[i - 1][1] > letter_ranges[i][0]){
// 重叠区间,保留一个最大的右边界值
letter_ranges[i][1] = Math.max(letter_ranges[i][1], letter_ranges[i - 1][1]);
}else{
// 当遇到非重叠区间时
// 计算划分的字符长度 = 重叠区间的最大右边界值 - 左边界值
ans.add(letter_ranges[i - 1][1] - left + 1);
// 更新左边界值
left = letter_ranges[i][0];
}
}
// 补充添加最后的一段区间长度
ans.add(letter_ranges[i - 1][1] - left + 1);
return ans;
}
}
当我洋洋得意的看了一遍题解之后,才发现原来还有更加巧妙的解题方法,详细的题解可查阅:《代码随想录》— 划分字母区间
Java解法(规律,利用字母区间的特点解题):
- 遍历字符串并记录每个字母的最大出现位置
- 划分的字母区间的右边界总满足其区间内字母的最大出现位置和下标相等
class Solution {
public List<Integer> partitionLabels(String s) {
List<Integer> ans = new ArrayList<>();
// 遍历字符串并记录每个字母的最大出现位置
int[] letter_rights = new int[27];
for(int i = 0; i < s.length(); i++){
letter_rights[s.charAt(i) - 'a'] = i;
}
int left = 0, right = 0;
for(int i = 0; i < s.length(); i++){
// 更新重叠区间内字母的最大出现位置
right = Math.max(right, letter_rights[s.charAt(i) - 'a']);
// 遇到划分点时,其下标与区间内字母的最大出现位置相等
if(i == right){
ans.add(i - left + 1);
// 更新重叠区间的左边界值
left = i + 1;
}
}
return ans;
}
}
合并区间
题目详细:LeetCode.56
经过前面重叠区间的练习以及绞尽脑汁的编码之后,再碰到这道题就觉得非常简单了:
- 按照区间的左边界从小到大对区间数组进行排序
- 定义一个变量left,记录重叠区间的左边界值
- 循环遍历每个区间并比较左右边界值
- 如果
当前区间的左边界值 < 上一个区间的右边界值
,则说明它们的区间是相交的,是重叠的区间,重叠的区间要保留一个最大的右边界值 - 否则说明当前区间与上一个区间不是相交的,开始进行划分,对之前的重叠区间进行合并
- 合并的区间,其左边界取 left,右边界取重叠区间中最大的右边界值
- 添加合并的区间到结果集中,注意还要更新变量left的值,以记录下一个重叠区间的左边界值
- 如果
- 当遍历到最后一个区间,最后的一个合并点刚好在数组尾部,但此时已结束循环,所以最后需要再补充对最后一段重叠区间进行合并并添加到结果集。
Java解法(重叠区间):
class Solution {
public int[][] merge(int[][] intervals) {
Arrays.sort(intervals, (a, b) -> {
return a[0] - b[0];
});
List<int[]> ans = new ArrayList<>();
int left = intervals[0][0], i = 1;
for(; i < intervals.length; i++){
if(intervals[i][0] <= intervals[i - 1][1]){
// 重叠区间,保留一个最大的右边界值
intervals[i][1] = Math.max(intervals[i - 1][1], intervals[i][1]);
}else{
ans.add(new int[]{left, intervals[i - 1][1]});
left = intervals[i][0];
}
}
ans.add(new int[]{left, intervals[i - 1][1]});
return ans.toArray(new int[0][]);
}
}