253. 会议室 II

难度中等317收藏分享切换为英文接收动态反馈
给你一个会议时间安排的数组 intervals ,每个会议时间都会包括开始和结束的时间 intervals[i] = [starti, endi] ,为避免会议冲突,同时要考虑充分利用会议室资源,请你计算至少需要多少间会议室,才能满足这些会议安排。

示例 1:
输入:intervals = [[0,30],[5,10],[15,20]] 输出:2
示例 2:
输入:intervals = [[7,10],[2,4]] 输出:1

提示:

  • 1 <= intervals.length <= 104
  • 0 <= starti < endi <= 106

思路一:模拟

1、记录开始时间和结束时间对应的会议序列,
set 记录相同开始时间下,不同的结束时间列表
outs记录相同结束时间下,不同的开始时间列表
2、遍历所有时间点
- list 存放正在运行的任务,
- room 记录当前会议室的用量
- 若当前时间是开始时间点,则用结束时间列表的长度来更新room变量。
- 若当前时间是结束时间点,则以当前时间点来过滤list列表,记录list列表前后的变化量,并更新到room变量

3、每个迭代,更新结果ans。

  1. /**
  2. * @param {number[][]} intervals
  3. * @return {number}
  4. */
  5. var minMeetingRooms = function(intervals) {
  6. const set = new Map(), outs = new Map(), times = new Set();
  7. for (let [s, e] of intervals) {
  8. times.add(s).add(e);
  9. if (!set.has(s)) {
  10. set.set(s, []);
  11. }
  12. set.get(s).push(e);
  13. if (!outs.has(e)) {
  14. outs.set(e, []);
  15. }
  16. outs.get(e).push(s);
  17. }
  18. let ans = 0, room = 0, list = [];
  19. const ps = [...times.keys()].sort((a, b) => a-b);
  20. // console.log(ps);
  21. for (let p of ps) {
  22. // console.log(p, room, ans, list);
  23. if (set.has(p)) { // start
  24. const outs = set.get(p);
  25. if (outs.length) {
  26. room += outs.length;
  27. outs.forEach((e) => list.push([p, e]));
  28. }
  29. }
  30. if (outs.has(p)){
  31. let oldLen = list.length;
  32. list = list.filter(([s, e]) => e > p);
  33. room -= (oldLen - list.length);
  34. }
  35. ans = Math.max(room, ans);
  36. }
  37. return ans;
  38. };

思路一:模拟 + 小空间优化:outs只记录结束时间,不记录开始时间,所以可以改用Set 来记录。

/**
 * @param {number[][]} intervals
 * @return {number}
 */
var minMeetingRooms = function(intervals) {
  const set = new Map(), outs = new Set(), times = new Set();
  for (let [s, e] of intervals) {
    times.add(s).add(e);
    if (!set.has(s)) {
      set.set(s, []);
    }
    set.get(s).push(e);

    outs.add(e);
  }

  let ans = 0, room = 0, list = [];
  const ps = [...times.keys()].sort((a, b) => a-b);
  // console.log(ps);
  for (let p of ps) {
    // console.log(p, room, ans, list);
    if (set.has(p)) { // start
      const outs = set.get(p);
      if (outs.length) {
        room += outs.length;
        outs.forEach((e) => list.push([p, e]));
      }
    }
    if (outs.has(p)){
      let oldLen = list.length;
      list = list.filter(([s, e]) => e > p);
      room -= (oldLen - list.length);
    }
    ans = Math.max(room, ans);
  }
  return ans;
};

思路一:模拟,set和outs的空间优化,
1、 观察上面的代码,发现,sets,和outs的values 并没有完全使用,后面只用到了values的长度,所以在前面构造数据的时候,直接计数;
2、times这个集合多余,也可以去除

/**
 * @param {number[][]} intervals
 * @return {number}
 */
var minMeetingRooms = function(intervals) {
  const set = new Map(), outs = new Map();
  for (let [s, e] of intervals) {
    if (!set.has(s)) {
      set.set(s, 1);
    } else {
      set.set(s, set.get(s) + 1);
    }
    if (!outs.has(e)) {
      outs.set(e, 1);
    } else {
      outs.set(e, outs.get(e) + 1);
    }
  }

  let ans = 0, room = 0;
  const ps = [...set.keys(), ...outs.keys()].sort((a, b) => a-b);
  for (let p of ps) {
    if (set.has(p)) { // start
      room += set.get(p);
    }
    if (outs.has(p)){
      let oldLen = outs.get(p);
      room -= oldLen;
    }
    ans = Math.max(room, ans);
  }
  return ans;
};

// 代码变形版本,便于观察,找规律,下一个版本将基于这个变形版本做优化。
/**
 * @param {number[][]} intervals
 * @return {number}
 */
var minMeetingRooms = function(intervals) {
  const ins = new Map(), outs = new Map(), times = new Set();
  for (let [s, e] of intervals) {
    times.add(s).add(e);
    ins.set(s, ins.has(s) ? (ins.get(s) + 1) : 1);
    outs.set(e, outs.has(e) ? (outs.get(e) + 1) : 1);
  }

  let ans = 0, room = 0;
  const ps = [...times.keys()].sort((a, b) => a-b);
  for (let p of ps) {
    room += ins.has(p) ? ins.get(p) : 0;
    room -= outs.has(p) ? outs.get(p) : 0;
    ans = Math.max(room, ans);
  }
  return ans;
};

思路二:模拟 + 增量计算

考虑上面版本的第二个变形写法,发现在模拟的循环中开始时加法运算,结束时剑法运算,考虑一个思路,将这个加加减减的增量提前到前半部分的时间点数据准备阶段完成,则可以在开始时间点的时候,+1计数,结束时间点的时候-1计数,这样两个时间点集合可以合并为一个times。最后代码如下。

/**
 * @param {number[][]} intervals
 * @return {number}
 */
var minMeetingRooms = function(intervals) {
  const times = new Map();
  for (let [s, e] of intervals) {
    times.set(s, times.has(s) ? (times.get(s) + 1) : 1);
    times.set(e, times.has(e) ? (times.get(e) - 1) : -1);
  }

  let ans = 0, room = 0;
  const ps = [...times.keys()].sort((a, b) => a-b);
  for (let p of ps) {
    room += times.has(p) ? times.get(p) : 0;
    ans = Math.max(room, ans);
  }
  return ans;
};

思路三:插旗法

考虑思路二的增量计算过程,发现times这个map似乎没有太强的必要性,决定结果大小的不是times这个容器的选择,而是数据计算顺序的选择,可以考虑将增量数据与存储方式剥离,简化代码逻辑。

时间序列和计数存储:选择简单的数组存储,每个会议存储[s,1],[e,-1], s和e分表代表第i会议的开始和结束时间,用变量times表示存储后的数组容器。

这样times数组中每个元素的第一个为时间点,第二个为该时间点的增量。

计算顺序:对times变量进行排序,由于要满足所有会议,所以贪心选择以开始时间非降序排序,如果开始时间相同,则以增量(结束时间,这里就两种值,-1和1,-1代表结束时间,1代表开始时间)的非降序排序,一句话,先开始先结束的优先安排会议室。

/**
 * @param {number[][]} intervals
 * @return {number}
 */
var minMeetingRooms = function(intervals) {
  const times = [];
  for (let [s, e] of intervals) {
    times.push([e, -1]);
    times.push([s, 1]);
  }
  const ps = times.sort((a, b) => {
    if (a[0] !== b[0]) {
      return a[0] - b[0];
    }
    return a[1] - b[1];
  });
  let ans = 0, room = 0;
  for (let [t, v] of ps) {
    room += v;
    ans = Math.max(room, ans);
  }
  return ans;
};

435. 无重叠区间

思路一:排序结束时间,从前向后遍历,计数不重叠区间。

/**
 * @param {number[][]} intervals
 * @return {number}
 */
var eraseOverlapIntervals = function(intervals) {
  intervals.sort((a, b) => a[1] - b[1]);
  let min = Number.NEGATIVE_INFINITY, ans = 0;
  for (let i = 0; i < intervals.length; i++) {
    if (intervals[i][0] >= min) {
      ans ++;
      min = intervals[i][1];
    }
  }
  return intervals.length - ans;
};

思路二:排序开始时间,从后向前遍历,计数不重叠区间

/**
 * @param {number[][]} intervals
 * @return {number}
 */
var eraseOverlapIntervals = function(intervals) {
  intervals.sort((a, b) => a[0] - b[0]);
  let limit = Number.POSITIVE_INFINITY, ans = 0;
  for (let i = intervals.length - 1; i >= 0; i--) {
    if (intervals[i][1] <= limit) {
      ans ++;
      limit = intervals[i][0];
    }
  }
  return intervals.length - ans;
};

452. 用最少数量的箭引爆气球

思路一:以结束时间排序,计数不重叠的区间,从前向后遍历,判断每个区间的开始时间大于前一个的结束时间

/**
 * @param {number[][]} points
 * @return {number}
 */
var findMinArrowShots = function(points) {
  points.sort((a, b) => a[1] - b[1]);
  let min = Number.MIN_SAFE_INTEGER;
  let ans = 0;
  for (let i = 0; i < points.length; i++) {
    if (points[i][0] > min) {
      ans += 1;
      min = points[i][1];
    }
  }
  return ans;
};

思路二:以开始时间排序,计数不重叠的区间,从后向前遍历,判断每个区间的结束时间小于后一个的开始时间

/**
 * @param {number[][]} points
 * @return {number}
 */
var findMinArrowShots = function(points) {
  points.sort((a, b) => a[0] - b[0]);
  let max = Number.MAX_SAFE_INTEGER;
  let ans = 0;
  for (let i = points.length - 1; i >= 0; i--) {
    if (points[i][1] < max) {
      ans += 1;
      max = points[i][0];
    }
  }
  return ans;
};

1353. 最多可以参加的会议数目

/**
 * @param {number[][]} events
 * @return {number}
 */
var maxEvents = function(events) {
  const pq = new MinPriorityQueue();
  let max = events[0][1];
  events.sort((a, b) => {
    max = Math.max(max, a[1], b[1]);
    return a[0] - b[0];
  });
  const push = (e) => pq.enqueue(e);
  const pop  = () => pq.dequeue();
  const peek = () => pq.front().element;
  const isEmpty = () => pq.isEmpty();

  let i = 0, res = 0, n = events.length;
  for (let d = 1; d <= max; d += 1) {
    // 弹出已经结束的会议
    while (!isEmpty() && peek() < d) {
      pop();
    }
    // 将开始时间为d的会议都入队列, 以结束时间最小堆为高优先级
    while (i < n && events[i][0] === d) {
      push(events[i][1]);
      i += 1;
    }

    // 弹出第d天可以选择的会议。
    if (!isEmpty()) {
      pop();
      res += 1;
    }
  }
  return res;
};