汇总区间
题目
版本1 正确
public List<String> summaryRanges(int[] nums) {
if (nums.length == 0) {
return new ArrayList<>();
}
// 汇总区间 将一个有序数组中的元素, 划分成不同的区间, 区间互不重叠, 并且囊括所有元素
int start = nums[0];
int end = nums[0];
List<String> ans = new ArrayList<>();
for (int i = 0; i < nums.length; i ++) {
end = nums[i];
if (i == nums.length - 1 || i + 1 < nums.length && nums[i + 1] != nums[i] + 1) {
// 记录一次
if (start == end) {
ans.add(String.valueOf(start));
} else {
ans.add(String.valueOf(start + "->" + end));
}
if (i + 1 < nums.length) {
// 更新start end
start = nums[i + 1];
}
}
}
return ans;
}
正确的原因
(1) 遇见差值大于1的元素, 就需要记录一次区间, 注意start和end指针的更新
合并区间
题目
版本1 正确 只对起点排序
public int[][] merge(int[][] intervals) {
if (intervals.length <= 1) {
return intervals;
}
// 合并数组中的重叠区间
// 用来存储合并后的区间
List<int []> ans = new ArrayList<>();
// 将二维数组, 按照start升序排列
Arrays.sort(intervals, new Comparator<int[]>() {
@Override
public int compare(int[] o1, int[] o2) {
return o1[0] - o2[0];
}
});
// 遍历一遍数组, 合并区间
int [] preArray = intervals[0];
for (int i = 1; i < intervals.length; i ++) {
if (intervals[i][0] <= preArray[1]) {
// 需要合并
preArray = new int[]{preArray[0], Math.max(preArray[1], intervals[i][1])};
// 对于最后一个区间 如果需要合并就添加合并的结果
if (i == intervals.length - 1) {
ans.add(preArray);
}
} else {
// 当前数组和前一个数组相比, 不需要合并了, 才添加上一个数组
ans.add(preArray);
// 对于最后一个数组
if (i == intervals.length - 1) {
ans.add(intervals[i]);
}
preArray = intervals[i];
}
}
// 将结果生成一个新的数组
int [][] ansArray = new int[ans.size()][];
for (int i = 0; i < ans.size(); i ++) {
ansArray[i] = ans.get(i);
}
return ansArray;
}
正确的原因
(1) 将区间按照start升序排列, 一定只能按start.
(2) 在合并区间取值的时候, 需要注意end要取两个区间的最大值
(3) 因为是在当前区间, 对于上一个区间进行添加到结果中, 因此对于最后一个区间要特别判断, 分两种情况(1) 最后一个区间能和上一个区间合并 (2) 最后一个区间不能和上一个区间合并
版本2 正确 对起点和终点都排序 并没有变快
public int[][] merge(int[][] intervals) {
if (intervals.length <= 1) {
return intervals;
}
// 合并区间, intervals中存在重叠部分的区间, 需要将重叠的部分合并, 返回一个包含不重叠区间的数组
// 对于区间问题, 无非就是将起点或者终点进行排序, 然后使得问题变成可以贪心择优
// 这里选择对起点进行升序排列, 对终点进行降序排列, 就可以在最少合并次数的情况下, 完成对区间的合并
Arrays.sort(intervals, new Comparator<int[]>() {
@Override
public int compare(int[] o1, int[] o2) {
if (o1[0] == o2[0]) {
// 起点相同, 按照终点降序
return o2[1] - o1[1];
} else {
return o1[0] - o2[0];
}
}
});
List<int []> ans = new ArrayList<>();
int [] preArray = intervals[0];
for (int i = 1; i < intervals.length; i ++) {
int [] nowArray = intervals[i];
if (nowArray[0] <= preArray[1] && nowArray[1] >= preArray[1]) {
// 当前的起点小于上一区间的终点 且 当前的终点大于上一区间的终点
// 当前区间需要和上一区间进行合并
preArray = new int[]{preArray[0], nowArray[1]};
} else if (nowArray[0] > preArray[1]){
// 当前区间无需和上一区间合并
// 此时将preArray添加到结果中, 作为一个有效的答案
ans.add(preArray);
preArray = nowArray;
}
// 对于最后一个区间, 需要单独考虑
if (i == intervals.length - 1) {
ans.add(preArray);
}
}
// 将结果变成数组
int [][] ansArray = new int[ans.size()][];
for (int i = 0; i < ans.size(); i ++) {
ansArray[i] = ans.get(i);
}
return ansArray;
}
(1) 对于起点和终点都进行排序, 然后判断是否合并的逻辑达到了最优, 但是实际执行速度并没有变快, 因为多了一次排序带来了更大的复杂度.
拼接视频
题目
版本1 错误 贪心策略不对
public int videoStitching(int[][] clips, int T) {
// 视频拼接 求将片段拼接成完成视频, 所需要的最小片段数目
// 即采用最少的拼接次数完成视频拼接
// 目标区间就是[0, 10]
// 但是可能拼接出大于目标区间的区间, 此时不再继续拼接, 可能拼接出[0, 10]后, 还有[7, 20]片段, 此时不再拼接
// 将区间的起点按照升序, 终点按照降序进行排序
Arrays.sort(clips, new Comparator<int[]>() {
@Override
public int compare(int[] o1, int[] o2) {
if (o1[0] == o2[0]) {
// 起点相同, 按照终点降序
return o2[1] - o1[1];
} else {
return o1[0] - o2[0];
}
}
});
int [] preArray = clips[0];
if (preArray[0] != 0) {
// 排序完后, 第一个区间的起点一定是0
return -1;
}
int count = 0;
for (int i = 1; i < clips.length; i ++) {
int [] nowArray = clips[i];
if (nowArray[0] <= preArray[1] && nowArray[1] >= preArray[1]) {
// 当前的起点小于上一区间的终点 且 当前的终点大于上一区间的终点
// 当前区间需要和上一区间进行合并
count ++;
preArray = new int[]{preArray[0], nowArray[1]};
if (preArray[0] == 0 && preArray[1] >= T) {
return count;
}
} else if (nowArray[0] > preArray[1]){
// 当前区间无法和上一区间合并
// 此时区间出现了断层, 如果preArray已经满足要求, 已经返回了, 不会执行到这里
// 因此执行到这里就肯定不对
return -1;
}
}
return -1;
}
错误的原因
(1) 我们选择在区间拼接的时候计数一次, 但是选择什么时机拼接, 并没有选择最优的贪心策略.
(2) 目前的策略是 当前区间的起点在上一区间中间且当前区间的终点在上一区间之外, 此时进行一次合并. 即[2, 5]会和[1, 4]合并成[1, 5]. 此时的贪心并不是最优的.
(3) 如果存在[1, 4], [2, 5], [3, 8] 这种区间组合, 那么[1, 4]不应该和[2, 5]合并一次, 而是应该直接和[3, 8]合并一次. 此时的贪心才是最优的, 即寻找起点在当前区间内, 同时终点最大的区间.
版本2 正确 最优贪心
public int videoStitching(int[][] clips, int T) {
if (T == 0) {
return 0;
}
// 视频拼接 求将片段拼接成完成视频, 所需要的最小片段数目
// 即采用最少的拼接次数完成视频拼接
// 目标区间就是[0, 10]
// 但是可能拼接出大于目标区间的区间, 此时不再继续拼接, 可能拼接出[0, 10]后, 还有[7, 20]片段, 此时不再拼接
// 将区间的起点按照升序, 终点按照降序进行排序
Arrays.sort(clips, new Comparator<int[]>() {
@Override
public int compare(int[] o1, int[] o2) {
if (o1[0] == o2[0]) {
// 起点相同, 按照终点降序
return o2[1] - o1[1];
} else {
return o1[0] - o2[0];
}
}
});
int [] preArray = clips[0];
if (preArray[0] != 0) {
// 排序完后, 第一个区间的起点一定是0
return -1;
}
if (preArray[1] >= T) {
return 1;
}
int count = 1;
int maxEnd = 0;
for (int i = 1; i < clips.length; i ++) {
int [] nowArray = clips[i];
// 最优的贪心策略, 即如果nowArray的起点在preArray范围内, 寻找终点最大的nowArray完成一次拼接
if (nowArray[0] >= preArray[0] && nowArray[0] <= preArray[1]) {
// 不断更新终点值
maxEnd = Math.max(maxEnd, nowArray[1]);
} else {
// 当前区间的起点不再在preArray的范围内的时候, 对最大值进行一次赋值
count ++;
preArray[1] = maxEnd;
if (maxEnd >= T) {
return count;
}
if (nowArray[0] > maxEnd) {
// 如果出现区间间隔, 直接返回-1
return -1;
}
// 当前元素需要再进行一次判断
i --;
}
}
// 对于数组最后的情况进行判断
if (maxEnd > preArray[1] && maxEnd >= T) {
return count + 1;
}
return -1;
}
正确的原因
(1) 明确最优策略是啥
(2) 实现的时候如何能够不遗漏情况
(3) 注意一些base case 例如
if (preArray[0] != 0) {
// 排序完后, 第一个区间的起点一定是0
return -1;
}
if (preArray[1] >= T) {
return 1;
}
插入区间
题目
版本1 正确 千万不要想着用二分
public int[][] insert(int[][] intervals, int[] newInterval) {
if (intervals.length == 0) {
int [][] temp = new int[1][];
temp[0] = newInterval;
return temp;
}
// 在一个按照起点排序好, 无重叠的区间数组中, 插入一个区间
// 如果插入后, 导致有重叠, 需要合并区间, 最终需要返沪无重叠的区间数组
// 因为原区间数组是无重叠的, 因此如果起点是排序好的, 终点也是排序好的
// 这道题不要用二分查找, 没办法提高查找的速率, 区间的二分查找非常麻烦, 且情况非常多
// 直接拿插入区间遍历一遍区间数组即可
List<int []> ans = new ArrayList<>();
int left = newInterval[0];
int right = newInterval[1];
for (int i = 0; i < intervals.length; i ++) {
int nowLeft = intervals[i][0];
int nowRight = intervals[i][1];
// 判断区间是否需要合并
if (left > nowRight || nowLeft > right || right == Integer.MAX_VALUE) {
if (nowLeft > right) {
// 此时需要添加lef到right这一段
ans.add(new int[]{left, right});
// 保证只添加一次
right = Integer.MAX_VALUE;
}
// 此时是没有交集的
// 添加一次结果
ans.add(intervals[i]);
continue;
} else {
// 区间是有交集的
// 更新left, right
left = Math.min(left, nowLeft);
right = Math.max(right, nowRight);
}
}
if (right != Integer.MAX_VALUE) {
ans.add(new int[]{left, right});
}
int [][] ansArray = new int[ans.size()][];
for (int i = 0; i < ans.size(); i ++) {
ansArray[i] = ans.get(i);
}
return ansArray;
}
正确的原因
(1) 注意什么时候添加[left, right]这个区间
无重叠区间
题目
版本1 正确
public int eraseOverlapIntervals(int[][] intervals) {
// 给定一个区间数组, 找到移除区间的最少数量 使得区间数组中的区间不再重叠
// 这里不要求例如剩下的区间覆盖的区域尽可能大的说法, 只要求剩余的区间不重叠
// 移除的最少数量, 其实就是剩余的不重叠的区间数量最大的时候
// 区间不重叠就是一个区间的起点 大于等于前一个区间的终点
// 剩余不重叠的区间最大, 就是让开头的那些区间的终点尽可能的小, 这样后续区间的起点大于终点的可能性才最大
// 才有可能选出来剩余最多的区间
// 将区间数组按照终点升序排序
Arrays.sort(intervals, new Comparator<int[]>() {
@Override
public int compare(int[] o1, int[] o2) {
return o1[1] - o2[1];
}
});
int count = 1;
int preEnd = intervals[0][1];
for (int i = 1; i < intervals.length; i ++) {
if (intervals[i][0] >= preEnd) {
count ++;
preEnd = intervals[i][1];
}
}
return intervals.length - count;
}
正确的原因
(1) 注意是按照终点升序排列