基础算法1 - 扫描线

968 阅读6分钟

概念 & 应用

概念:不需要检测每一时刻,只需要检测起点或者终点的位置!(交点变化的位置只有起点 或者 终点

应用:

  • 区间问题


区间问题

区间问题肯定按照区间的起点或者终点进行排序!!!

「区间问题」template:“上下车

  • 解法一:将(start, +val)(end, -val)sort后进行扫描线
  • 解法二:priority queue(heap)

391. 数飞机(Medium)

image.png

Solu:扫描线上 - 下车问题

image.png

Code:

class Solution:
    def countOfAirplanes(self, airplanes):
        arr = []
        for interval in airplanes:
            arr.append((interval.start, 1))
            arr.append((interval.end, -1))
        arr.sort()  # 时间相同时,降落优先于起飞
        count, ans = 0, 0
        for i in range(len(arr)):
            count += arr[i][1]
            ans = max(ans, count)
        return ans


252. 会议室(Easy)

image.png

Solu 1:扫描线+上下车

同上,略

Code 1:

class Solution:
    def canAttendMeetings(self, intervals: List[List[int]]) -> bool:
        arr = []
        for start, end in intervals:
            arr.append((start, 1))
            arr.append((end, -1))
        arr.sort()
        count = 0
        for _, v in arr:
            count += v
            if count > 1:
                return False
        return True

Solu 2:

image.png

看是否存在meeting2's start < meeting1's end(严格小于)

Code 2:

class Solution:
    def canAttendMeetings(self, intervals: List[List[int]]) -> bool:
        intervals.sort(key=lambda x: x[0])
        for i in range(1, len(intervals)):
            if intervals[i][0] < intervals[i - 1][1]:
                return False
        return True


253. 会议室 II(Medium)

image.png

Solu 1:扫描线 + 上下车问题

max{#同一时刻需要的会议室} = min{#能够满足整个会议安排的会议室}

Code 1:

class Solution:
    def minMeetingRooms(self, intervals: List[List[int]]) -> int:
        arr = []
        for start, end in intervals:
            arr.append((start, 1))
            arr.append((end, -1))
        arr.sort()
        rooms = 0
        ans = 0
        for _, v in arr:
            rooms += v
            ans = max(rooms, ans)
        return ans

Solu 2:双指针

image.png

如果存在overlap(start[i] < end[j]) -> 需要新开一个会议室

Code 2:双指针 + 上下车

class Solution:
    def minMeetingRooms(self, intervals: List[List[int]]) -> int:
        start_arr, end_arr = [], []
        for start, end in intervals:
            start_arr.append(start)
            end_arr.append(end)
        start_arr.sort()
        end_arr.sort()
        start, end, room, ans = 0, 0, 0, 0
        while start < len(start_arr) and end < len(end_arr):
            if start_arr[start] < end_arr[end]:  # overlap -> use a new meeting room
                room += 1
                start += 1
            else:
                room -= 1
                end += 1
            ans = max(ans, room)
        return ans

或者

class Solution:
    def minMeetingRooms(self, intervals: List[List[int]]) -> int:
        start_arr, end_arr = [], []
        for start, end in intervals:
            start_arr.append(start)
            end_arr.append(end)
        start_arr.sort()
        end_arr.sort()
        end, room = 0, 0
        for start in start_arr:
            if start < end_arr[end]:
                room += 1
            else:  # no overlap -> reuse this room
                end += 1
        return room


56. 合并区间(Medium)

image.png

Solu:

image.png

对整个intervals按照start_time进行sort(不需要考虑end_time

if overlaps:
    merge intervals
else:
    insert a new interval

Code:

class Solution:
    def merge(self, intervals: List[List[int]]) -> List[List[int]]:
        intervals.sort()
        merged = []
        cur = intervals[0]
        for next in intervals[1:]:
            if next[0] <= cur[1]:  # overlap -> merge
                cur[1] = max(cur[1], next[1])
            else:
                merged.append(cur)
                cur = next
        return merged + [cur]


57. 插入区间(Medium)

image.png

Solu:

  • 时间还太早cur[1] < newInterval[0]cur还没有和newInterval产生关联,直接append cur
  • 时间过去了cur[0] > newInterval[1]:后续的所有intervals都不会再和newInterval产生交集,直接append
  • 有overlap,merge newInterval and cur

Code:

class Solution:
    def insert(self, intervals: List[List[int]], newInterval: List[int]) -> List[List[int]]:
        intervals.append([sys.maxsize, sys.maxsize])  # add sentinel
        res = []
        for i, cur in enumerate(intervals):
            if cur[1] < newInterval[0]:
                res.append(cur)
            elif cur[0] > newInterval[1]:
                res.append(newInterval)
                res += intervals[i:]
                break
            else:
                newInterval[0] = min(cur[0], newInterval[0])
                newInterval[1] = max(cur[1], newInterval[1])
        return res[:-1]


1272. 删除区间(Medium)

image.png

Solu:

  • no overlap -> 不会被影响,直接append cur
  • 有overlap -> 分别去append cur中没有被toBeRemoved cover掉的头和尾

Code:

class Solution:
    def removeInterval(self, intervals: List[List[int]], toBeRemoved: List[int]) -> List[List[int]]:
        res = []
        for cur in intervals:
            if cur[0] > toBeRemoved[1] or cur[1] < toBeRemoved[0]:  # no overlap
                res.append(cur)
            else:  # overlap
                if cur[0] < toBeRemoved[0]:
                    res.append([cur[0], toBeRemoved[0]])
                if cur[1] > toBeRemoved[1]:
                    res.append([toBeRemoved[1], cur[1]])
        return res


435. 无重叠区间(Medium)

image.png

Solu 1:Greedy + 反向思考

min{#使整个区间没有重叠的移除区间} = max{不重叠区间}

  • 按照每个intervalend进行sort
  • 把所有和当前区间cur有overlap的intervals都remove

image.png

image.png

Code 1:

class Solution:
    def eraseOverlapIntervals(self, intervals: List[List[int]]) -> int:
        intervals = sorted(intervals, key=lambda x: x[1])  # 按照end进行sort
        count = 1  # 最大不重叠区间数
        curEnd = intervals[0][1]
        for i in range(1, len(intervals)):
            if intervals[i][0] >= curEnd:  # no overlap
                count += 1
                curEnd = intervals[i][1]
        return len(intervals) - count

Solu2:Greedy + 正向思考

  • if two conflicts, always remove the later one, in order to leave more space for the later.

Code 2:

class Solution:
    def eraseOverlapIntervals(self, intervals: List[List[int]]) -> int:
        intervals = sorted(intervals, key=lambda x: x[1])
        count = 0
        curEnd = intervals[0][1]
        for i in range(1, len(intervals)):
            if intervals[i][0] >= curEnd:  # no overlap
                curEnd = intervals[i][1]
            else:
                count += 1
        return count


1288. 删除被覆盖区间(Medium)

image.png

Solu:

image.png

intervals sort by:

  • start time increasing
  • end time decreasing

保证只有index小的可以覆盖index大的

Code:

class Solution:
    def removeCoveredIntervals(self, intervals: List[List[int]]) -> int:
        intervals.sort(key=lambda x: (x[0], -x[1]))
        count = 0  # #intervals need to be removed
        curEnd = intervals[0][1]
        for i in range(1, len(intervals)):
            if intervals[i][1] <= curEnd:
                count += 1
            else:
                curEnd = intervals[i][1]
        return len(intervals) - count


352. 将数据流变为多个不相交区间(Hard)

image.png

image.png

Solu:

3种case:

image.png

  • 需要add this new interval x3:
    • merge with both prev and next
    • merge with prev
    • merge with next
  • 不需要add x2:
    • 本身已经含有这个interval
    • 这个interval被prev或者next完全cover了

Code:

class SummaryRanges {

  private TreeSet<int[]> set = null;

  public SummaryRanges() {
    set = new TreeSet<>((a, b) -> a[0] == b[0] ? a[1] - b[1] : a[0] - b[0]);
  }

  public void addNum(int val) {
    int[] interval = new int[] {val, val};
    int[] prev = set.lower(interval), next = set.higher(interval);
    if (set.contains(interval)
        || (prev != null && prev[1] >= val)
        || (next != null && next[0] <= val)) return; // no need to add
    if (prev != null
        && prev[1] + 1 == val
        && next != null
        && next[0] - 1 == val) { // merge two intervals
      prev[1] = next[1];
      set.remove(next);
    } else if (prev != null && prev[1] + 1 == val) prev[1] = val; // extend prev interval
    else if (next != null && next[0] - 1 == val) next[0] = val; // extend next interval
    else set.add(interval); // add new interval
  }

  public int[][] getIntervals() {
    List<int[]> list = new ArrayList<>();
    set.forEach(a -> list.add(a));
    return list.toArray(new int[list.size()][]);
  }
}


Common Slot

双指针i, j分别遍历slots1slots2

1229. 安排会议日程(Medium)

image.png

Solu:双指针

找到第一个valid common slot

相离:

image.png

相交:

image.png

包含:

image.png

包含:

image.png

相交:

image.png

相离:

image.png

  • case 1,6:相离,没有相交的区间
  • case 2,3:A 的尾永远大于 B 的尾。所以相交的区间为 [min(A[0], B[0]), B[1]]
  • case 4,5:B 的尾永远大于 A 的尾。所以相交的区间为 [min(A[0], B[0]), A[1]]

Code:

class Solution:
    def minAvailableDuration(self, slots1: List[List[int]], slots2: List[List[int]], duration: int) -> List[int]:
        slots1.sort()
        slots2.sort()
        i, j = 0, 0
        while i < len(slots1) and j < len(slots2):
            start, end = max(slots1[i][0], slots2[j][0]), min(slots1[i][1], slots2[j][1])
            if end - start >= duration:
                return [start, start + duration]
            # not overlapping or current intersection is too small
            if slots1[i][1] < slots2[j][1]:
                i += 1
            else:
                j += 1
        return []


986. 区间列表的交集(Medium)

image.png

Solu:

双指针,分析同上。略

Code:

class Solution:
    def intervalIntersection(self, firstList: List[List[int]], secondList: List[List[int]]) -> List[List[int]]:
        i, j = 0, 0
        res = []
        while i < len(firstList) and j < len(secondList):
            start, end = max(firstList[i][0], secondList[j][0]), min(firstList[i][1], secondList[j][1])
            if start <= end:  # exist intersection
                res.append([start, end])
            if firstList[i][1] > secondList[j][1]:
                j += 1
            else:
                i += 1
        return res


759. 员工空闲时间(Hard)

image.png

Solu:

  • 将所有interval按照start升序排序
  • 一旦curInterval.end < nextInterval.start,则必然有空隙[curInterval.end, nextInterval.start](因为nextNextInterval.start ≥ nextInterval.start必然成立)

image.png

Code:

class Interval:
    def __init__(self, start: int = None, end: int = None):
        self.start = start
        self.end = end


class Solution:
    def employeeFreeTime(self, schedule: '[[Interval]]') -> '[Interval]':
        pq, res = [], []
        for intervals in schedule:
            for interval in intervals:
                heapq.heappush(pq, [interval.start, interval.end])
        cur_end = heapq.heappop(pq)[1]
        while pq:
            next_start, next_end = pq[0]
            if next_start > cur_end:  # 必然存在空隙, 加入答案
                res.append(Interval(cur_end, next_start))
                cur_end = heapq.heappop(pq)[1]
            else:  # 不存在空隙, merge两个区间
                cur_end = max(cur_end, heapq.heappop(pq)[1])
        return res


天际线

218. 天际线问题(Hard)

image.png

image.png

Solu:

  • 根据位置进行sort;相同位置下高度最大的楼层优先访问
  • 遇到一个楼“开始”(height > 0),加入pq;遇到一个楼“结束”(height < 0),把这幢楼的高度从pq中删除
  • 如果删除/添加一栋楼,会使得当前最大高度curMax产生变化,则一条新的skyline产生

image.png

Code:

class Solution {

  public List<List<Integer>> getSkyline(int[][] buildings) {
    List<List<Integer>> res = new ArrayList<>();
    List<int[]> heights = new ArrayList<>();
    for (int[] building : buildings) {
      heights.add(new int[] {building[0], building[2]});
      heights.add(new int[] {building[1], -building[2]});
    }
    heights.sort((a, b) -> a[0] == b[0] ? b[1] - a[1] : a[0] - b[0]); // 当同一位置上,楼层最高的先被访问
    PriorityQueue<Integer> pq = new PriorityQueue<>((a, b) -> b - a);
    pq.offer(0); // sentinel
    int prevMax = 0;
    for (int[] height : heights) {
      if (height[1] > 0) pq.offer(height[1]); // 遇到新房子,加入pq,从高到低
      else pq.remove(-height[1]); // 遇到老房子,从pq中删除
      int curMax = pq.peek();
      if (prevMax != curMax) { // 检查是否对当前的最大高度产生影响
        res.add(Arrays.asList(height[0], curMax)); // 如果是,则一条新的skyline产生
        prevMax = curMax;
      }
    }
    return res;
  }
}


几何

391. 完美矩形(Hard)

image.png

Solu:HashSet

image.png

  • 「完美矩形」的性质 x2:
    • 正好4个「外围顶点」(不与其他任何顶点重叠或覆盖)
    • ∑area = S(4个角落顶点形成的封闭区域)

Code:

class Solution:
    def isRectangleCover(self, rectangles: List[List[int]]) -> bool:
        # validity:4个"外围顶点" + ∑area = 4个角落顶点形成的封闭区域的面积
        seen = set()
        areas = 0
        for i, rect in enumerate(rectangles):
            p1, p2, p3, p4 = (rect[0], rect[1]), (rect[2], rect[1]), (rect[2], rect[3]), (rect[0], rect[3])
            points = [p1, p2, p3, p4]
            for point in points:
                if point in seen:
                    seen.remove(point)
                else:
                    seen.add(point)
            areas += (rect[2] - rect[0]) * (rect[3] - rect[1])
        if len(seen) != 4:
            return False
        seen = sorted(seen, key=lambda x: (x[0], x[1]))
        return areas == (seen[3][0] - seen[0][0]) * (seen[3][1] - seen[0][1])


Reference: