1.会议室(252-easy)
题目描述:给定一个会议时间安排的数组 intervals ,每个会议时间都会包括开始和结束的时间 intervals[i] = [starti, endi] ,请你判断一个人是否能够参加这里面的全部会议,即判断是否存在重叠区域!
示例:
输入: intervals = [[0,30],[5,10],[15,20]] 输出: false 解释: 存在重叠区间,一个人在同一时刻只能参加一个会议。
思路:对每个会议时间按照开始时间排序(关键)。然后遍历数组进行判断,如果前一个会议结束的时间大于后一个会议开始的时间(前一个还没结束,后一个就开始了),则存在重叠区域。
代码:
public boolean canAttendMeetings(int[][] intervals) { if (intervals == null || intervals.length == 0) return false; /** Arrays.sort(intervals, new Comparator<int[]>() { @Override public int compare(int[] o1, int[] o2) { return o1[0] - o2[0]; } }); */ Arrays.sort(intervals, (a, b) -> a[0] - b[0]); for (int i = 1; i < intervals.length; i++) { // 前一个结束时间大于后一个开始时间 if (intervals[i][0] < intervals[i - 1][1]) { returtn false; } } return true; }
拓展1:会议室II 253,给定一个会议时间安排的数组,每个会议时间都会包括开始和结束的时间 [[s1,e1],[s2,e2],…] (si < ei),为避免会议冲突,同时要考虑充分利用会议室资源,请你计算至少需要多少间会议室,才能满足这些会议安排。
示例:
示例 1: 输入: [[0, 30],[5, 10],[15, 20]] 输出: 2 示例 2: 输入: [[7,10],[2,4]] 输出: 1
思路:有时间重叠的,肯定不能安排在一间会议室。还是需要先排序,这里按照会议的开始时间排序,这里使用小根堆(优先级队列)存储会议的结束时间(堆顶为会议最早结束时间):
- 如果另一场会议的开始时间小于当前堆顶,说明会议时间冲突,我们需要再单独开一间(即当前会议的结束时间作为新的堆顶);
- 否则,没有时间冲突,等这场会议结束使用。
代码:
import java.util.LinkedList; class Solution { public int minMeetingRooms(int[][] intervals) { if(intervals == null || intervals.length == 0){ return 0; } Arrays.sort(intervals, (a, b) -> a[0] - b[0]); PriorityQueue<Integer> queue = new PriorityQueue<>((o1, o2) -> o1 - o2); queue.offer(intervals[0][1]); for(int i = 1; i < intervals.length; i++){ if(intervals[i][0] >= queue.peek()){ queue.poll(); } queue.offer(intervals[i][1]); } return queue.size(); } }
拓展2:最多可以参加的会议数目1353,给你一个数组 events,其中 events[i] = [startDayi, endDayi] ,表示会议 i 开始于 startDayi ,结束于 endDayi 。
你可以在满足 startDayi <= d <= endDayi 中的任意一天 d 参加会议 i 。注意,一天只能参加一个会议。请你返回你可以参加的 最大 会议数目。
示例:
示例 1: 输入:events = [[1,2],[2,3],[3,4]] 输出:3 解释:你可以参加所有的三个会议。 安排会议的一种方案如上图。 第 1 天参加第一个会议。 第 2 天参加第二个会议。 第 3 天参加第三个会议。
思路:本题还是排序,注意题意,我们不是要把一个会议的所有天都在,只需要参加满足会议(在开始和结束之间参加)的一天即可,比较简单的是利用set存储已经占用的天数(因为同一天不能参加两个会议),但是超时。。。
可以使用优先级队列,将这一天能参加的会议的结束时间全部入堆,会议的起始时间 == day,这一天一定能参加(注意按照会议的开始时间排序)。为什么是结束时间?有点贪心思想,保证能参加最多的会议
- 我们一天一天安排,首先删除已经结束的会议(出堆),此时堆顶这一天是我们可以参加的会议(一天只能参加一场)!
代码:
class Solution { // public int maxEvents(int[][] events) { // Arrays.sort(events, (o1, o2) -> o1[1] - o2[1]); // Set<Integer> set = new HashSet<>(); // for (int[] event : events) { // for (int i = event[0]; i <= event[1]; ++i) { // if (!set.contains(i)) { // set.add(i); // break; // } // } // } // return set.size(); // } public int maxEvents(int[][] events) { Arrays.sort(events, (o1, o2) -> o1[0] - o2[0]); PriorityQueue<Integer> pq = new PriorityQueue<>(); int i = 0; int day = 1; int ans = 0; int n = events.length; while (i < n || !pq.isEmpty()) { while (i < n && events[i][0] == day) { pq.offer(events[i][1]); i++; } while (!pq.isEmpty() && pq.peek() < day) { pq.poll(); } if (!pq.isEmpty()) { pq.poll(); ans++; } day++; } return ans; } }
2.不重叠区间(435-medium)
题目描述:给定一个区间的集合,找到需要移除区间的最小数量,使剩余区间互不重叠。
示例:
输入: [ [1,2], [2,3], [3,4], [1,3] ] 输出: 1 解释: 移除 [1,3] 后,剩下的区间没有重叠。
思路:为了保证移除最少的区间集合,即寻找最多的不重叠区间。贪心策略:保证每个区间尾越小,那么后边预留的空间越大。
- 可以通过对尾区间进行排序,遍历数组(记录不重叠个数
count
),当前头小于上一个的尾,直接删除(重叠), - 否则
count++
,更新最小的尾区间end。
ps:注意起始边界处理,时间复杂度:O(nlogn)!
代码:
public int eraseOverlapIntervals(int[][] intervals) { if (intervals == null || intervals.length == 0) return 0; Arrays.sort(intervals, (a, b) -> a[1] - b[1]); //1.每个子区间尾排序 int count = 1; int end = intervals[0][1]; for (int i = 1; i < intervals.length; i++) { if (intervals[i][0] < end) continue; //2.与之前区间重叠(即当前区间的头小于之前区间的尾,删除) end = intervals[i][1]; count++; } return intervals.length - count; //3.要删除的区间数量 }
3.合并区间(56-medium)
题目描述:给出一个区间的集合,请合并所有重叠的区间。
示例:
输入: intervals = [[1,3],[2,6],[8,10],[15,18]] 输出: [[1,6],[8,10],[15,18]] 解释: 区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6]. 输入: intervals = [[1,4],[4,5]] 输出: [[1,5]] 解释: 区间 [1,4] 和 [4,5] 可被视为重叠区间。
思路:实现合并区间肯定要进行排序。采用每个子区间左边界排序(右边界也可),然后从左向右遍历数组。注意:
- 若没有重叠(数组为空/当前区间的左边界,大于结果区间的右边界),不需合并则加入结果;
- 否则更新右边界生成新的区间加入结果,更新结果区间的右边界。
代码:
public int[][] merge(int[][] intervals) { //1.按照每个子区间端点(左端点)进行排序 Arrays.sort(intervals, (a, b) -> a[0] - b[0]); int[][] ret = new int[intervals.length][2]; int idx = -1; for (int[] interval : intervals) { //3.没有重叠(数组为空/当前区间左边界大于结果区间右边界),直接加入 if (idx == -1 || interval[0] > ret[idx][1]) { ret[++idx] = interval; } else { //4.有重叠,合并区间(更新结果区间右边界) ret[idx][1] = Math.max(interval[1], ret[idx][1]); } } //ps:copyOf(int[] original, int newLength) return Arrays.copyOf(ret, idx + 1); }
4.插入区间(57-medium)
题目描述:给出一个无重叠的 ,按照区间起始端点排序的区间列表。在列表中插入一个新的区间,你需要确保列表中的区间仍然有序且不重叠(如果有必要的话,可以合并区间)。
示例:
输入:intervals = [[1,3],[6,9]], newInterval = [2,5] 输出:[[1,5],[6,9]] 输入:intervals = [[1,2],[3,5],[6,7],[8,10],[12,16]], newInterval = [4,8] 输出:[[1,2],[3,10],[12,16]] 解释:这是因为新的区间 [4,8] 与 [3,5],[6,7],[8,10] 重叠。
思路:因为原始数组已经排好序,并且不存在重叠,那么直接用索引i遍历数组,索引idx记录结果集索引,分三步走:
- 如果存在,将左边与新区间不重叠的部分直接接入结果(没影响的部分);
- 新区间与区间存在重叠,合并区间(更新新区间左右边界),将更新后的新区间加入结果;通俗一点说:新区间的左边界<= 当前上一个区间的右边界,>= 下一个区间的左边界,我们需要将这三个区间合并(更新新区间的左右边界,加入结果)
- 如果存在,将右边与新区间不重叠的部分直接接入结果(没影响的部分);
代码:
public int[][] insert(int[][] intervals, int[] newInterval) { int[][] ret = new int[intervals.length + 1][2]; int idx = 0, i = 0; //1.左边不重叠区间 while (i < intervals.length && newInterval[0] > intervals[i][1]) { ret[idx++] = intervals[i++]; } //2.区间合并(更新新区间的左右边界) while (i < intervals.length && newInterval[1] >= intervals[i][0]) { newInterval[0] = Math.min(newInterval[0], intervals[i][0]); newInterval[1] = Math.max(newInterval[1], intervals[i][1]); i++; } ret[idx++] = newInterval; //3.右边不重叠区间 while (i < intervals.length) { ret[idx++] = intervals[i++]; } return Arrays.copyOf(ret, idx); }
总结
总结上述四个题目,主要可以细分为两类问题。
涉及区间重叠问题:
- 区间是否重叠(T252)
- 最多的的不重叠区间(T435)
涉及区间合并问题:
- 合并所有重叠区间(T56)
- 合并插入过程中可能引入的重叠区间(T57)
其实,上述问题都需要对区间端点进行排序,这里明确一点,不管是根据左端点排序还是右端点排序都是可以的。需要画图去看左右边界情况,比较直观。