基础算法一:扫描线

85 阅读8分钟

1. 数飞机

给出飞机的起飞和降落时间的列表,用序列 `interval` 表示. 请计算出天上同时最多有多少架飞机?
如果多架飞机降落和起飞在同一时刻,我们认为降落有优先权。
输入: [(1, 10), (2, 3), (5, 8), (4, 7)] 
输出: 3 
解释: 第一架飞机在1时刻起飞, 10时刻降落. 
     第二架飞机在2时刻起飞, 3时刻降落. 
     第三架飞机在5时刻起飞, 8时刻降落. 
     第四架飞机在4时刻起飞, 7时刻降落. 
     在5时刻到6时刻之间, 天空中有三架飞机.
public class Solution {

    // 自己定义了一个Point:这个对象也可以是数组,list,map等等
    public class Point {
      int T, S;
      Point(int T, int S) {
          this.T = T;   // 起飞或降落时间
          this.S = S;   // 设置权重,用于排序,时间相同时优先降落
      }
    }

    public int countOfAirplanes(List<Interval> airplanes) {
        List<Point> list = new ArrayList<>();
        for ( Interval Interval : airplanes ) {
            // 遇到起飞点+1
            list.add(new Point(Interval.start , 1));
            // 遇到降落点-1
            list.add(new Point(Interval.end , -1));
        }
        // 排序:形成时间线:按时间升序排列
        Collections.sort(list , (Point p1 , Point p2) -> {
            // 如果当前时间冲突,降落的优先
            if(p1.T == p2.T) return p1.S - p2.S;
            return p1.T - p2.T;
        });
        
        // 进行扫描
        int ans = 0;
        int cnt = 0;
        for(Point point : list){
            cnt = cnt + point.S;
            ans = Math.max(ans , cnt);
        }
        return ans;
    }
}

2. 罗志祥的多人运动

已知小猪每晚都要约好几个女生到酒店房间.每个女生i与小猪约好的时间由【si , ei】表示,
其中si表示女生进入房间的时间,ei 表示女生离开房间的时间.
由于小猪心胸开阔,思想开明,不同女生可以同时存在于小猪的房间.
请计算出小猪最多同时在做几人的「多人运动」.

Input[ [0 , 30] , [5 , 10], [15, 20] ]
OutPut :最多同时有两个女生的「三人运动」
public class Solution {
    public int countOfAirplanes(List<Interval> sportList) {
        List<int[]> list = new ArrayList<>();
        // 首先将起始点和结束点分别设置权重
        for (Interval interval : sportList) {
            int[] startArr = new int[2];
            startArr[0] = interval.si;   // 开始时间点
            startArr[1] = 1;    // 设置权重
            int[] endArr = new int[2];
            endArr[0] = interval.ei;    // 结束时间点
            endArr[1] = -1;     // 设置权重
            list.add(startArr);
            list.add(endArr);
        }
        // 排序生成扫描线
        Collections.sort(list, (a, b) -> {
            if (a[0] == b[0]) return a[1] - b[1];
            return a[0] - b[0];
        });
        // 进行扫描
        int ant = 0;
        int cur = 0;
        for (int[] val : list) {
            cur += val[1];
            ant = Math.max(ant , cur);
        }
        return ant;
    }
}

3. 会议室

给定一个会议时间安排的数组 intervals ,每个会议时间都会包括开始和结束的时间 
intervals[i] = [starti, endi],请你判断一个人是否能够参加这里面的全部会议

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

示例2:
输入:intervals = [[7,10],[2,4]]
输出:true
public class Solution {
    public boolean canAttendMeetings(Interval[] intervals) {
        // 首先进行排序生成扫秒线
        Arrays.sort(intervals, (a, b) -> {
            return a.starti - b.starti;
        });
        // 扫描,如果i的结束时间大于i+1的开始时间,那就不能参加全部会议
        for (int i = 0; i < intervals.length - 1; i++) {
            if (intervals[i].endi > intervals[i + 1].starti) {
                return false;
            }
        }
        return true;
    }
}

4. 会议室II

给你一个会议时间安排的数组 intervals ,每个会议时间都会包括开始和结束的时间 
intervals[i] = [starti, endi],为避免会议冲突,同时要考虑充分利用会议室资源,
请你计算至少需要多少间会议室,才能满足这些会议安排.

示例:
输入:intervals = [[0,30],[5,10],[15,20]]
输出:2
public class Solution {
    public int minMeetingRooms(List<Interval> intervals) {
        // 其实这个问题就是数飞机,要用多少会议室,你就统计同一时间最多在开几个会
        List<int[]> list = new ArrayList<>();
        for (Interval interval : intervals) {
            list.add(new int[]{interval.start, 1});
            list.add(new int[]{interval.end, -1});
        }
        // 排序生成扫描线
        Collections.sort(list, (a, b) -> {
            if (a[0] == b[0]) return a[1] - b[1];  // 先出后进:不会重叠
            return a[0] - b[0];
        });
        // 扫描计数
        int cur = 0;
        int ant = 0;
        for (int[] ints : list) {
            cur += ints[1];
            ant = Math.max(ant, cur);
        }
        return ant;
    }
}

5. 合并区间

以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi] 
请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间 

示例 1:
输入:intervals = [[1,3],[2,6],[8,10],[15,18]]
输出:[[1,6],[8,10],[15,18]]
解释:区间 [1,3][2,6] 重叠, 将它们合并为 [1,6].
public class Solution {
    public int[][] merge(int[][] intervals) {
        List<int[]> list = new ArrayList<>();
        // 排序生成扫描线
        Arrays.sort(intervals , (a,b) -> a[0]-b[0]);
        int[] cur = intervals[0];
        for (int i = 1; i < intervals.length; i++) {
            if (cur[1] > intervals[i][0]){
                cur[1] = Math.max(cur[1] , intervals[i][1]);
            }else {
                list.add(cur);
                cur = intervals[i];
            }
        }
        // 把最后一个合并区间放进来
        list.add(cur);
        return list.toArray(new int[0][]);
    }
}

6. 插入区间

给你一个无重叠的,按照区间起始端点排序的区间列表
在列表中插入一个新的区间,你需要确保列表中的区间仍然有序且不重叠
(如果有必要的话,可以合并区间)

示例1:
输入:intervals = [[1,3],[6,9]], newInterval = [2,5]
输出:[[1,5],[6,9]]

示例2:
输入: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] 重叠。
class Solution {
    /**我自己的题解*/
    public int[][] insert(int[][] intervals, int[] newInterval) {
        List<int[]> list = new ArrayList<>();
        list.add(newInterval);
        for(int[] interval : intervals){
            list.add(interval);
        }
        // 生成扫描线
        Collections.sort(list , (a,b)->{
            return a[0]-b[0];
        });
        List<int[]> res = new ArrayList<>();
        int n = list.size();
        int[] cur = list.get(0);
        for(int i=1 ; i<n ; i++){
            if (cur[1] >= list.get(i)[0] ){
                cur[1] = Math.max(cur[1] , list.get(i)[1]);
            }else{
                res.add(cur);
                cur = list.get(i);
            }
        }
        res.add(cur);
        return res.toArray(new int[res.size()][0]);
    }
    
    /**官方题解*/
        public int[][] insert(int[][] intervals, int[] newInterval) {
        List<int[]> res = new ArrayList<>();
        for(int[] cur : intervals){
            if( newInterval == null || cur[1] <  newInterval[0]){
                res.add(cur);
            }else if(cur[0] > newInterval[1]){
                res.add(newInterval);
                res.add(cur);
                newInterval = null;
            }else{
                // 合并
                newInterval[0] = Math.min(newInterval[0] , cur[0]);
                newInterval[1] = Math.max(newInterval[1] , cur[1]);
            }
        }
        if(newInterval != null){
            res.add(newInterval);
        }
        return res.toArray(new int[res.size()][0]);
    }
}

7. 删除区间

给你一个有序的不相交区间列表 intervals 和一个要删除的区间 toBeRemoved,
intervals 中的每一个区间 intervals[i] =[a, b] 都表示满足 a<=x<b 的所有实数x的集合.
我们将 intervals 中任意区间与 toBeRemoved 有交集的部分都删除。
返回删除所有交集区间后,intervals剩余部分的有序列表。

示例 1:
输入:intervals = [[0,2],[3,4],[5,7]], toBeRemoved = [1,6]
输出:[[0,1],[6,7]]
public List<List<Integer>> removeInterval(int[][] intervals, int[] toBeRemoved) {
    List<List<Integer>> result = new ArrayList<>();
    for (int[] interval : intervals) {
        if (interval[0] > toBeRemoved[1] || interval[1] < toBeRemoved[0]) {
            result.add(Arrays.asList(interval[0], interval[1]));
        } else {
            if (interval[0] < toBeRemoved[0]) {
                result.add(Arrays.asList(interval[0], toBeRemoved[0]));
            }
            if (interval[1] > toBeRemoved[1]) {
                result.add(Arrays.asList(toBeRemoved[1], interval[1]));
            }
        }
    }
    return result;
}

8. 无重叠区间

给定一个区间的集合intervals,其中 intervals[i] = [starti, endi].
返回需要移除区间的最小数量,使剩余区间互不重叠.

示例 1:
输入: intervals = [[1,2],[2,3],[3,4],[1,3]]
输出: 1
解释: 移除 [1,3] 后,剩下的区间没有重叠
public int eraseOverlapIntervals(int[][] intervals) {
    Arrays.sort(intervals , (a,b)->a[0]-b[0]);
    int[] cur = intervals[0];
    int sum=0;
    for (int i = 1; i < intervals.length; i++) {
        if (cur[1] >intervals[i][0]){
            sum++;
            cur[1] = Math.min(cur[1] , intervals[i][1]);
        }else {
            cur = intervals[i];
        }
    }
    return sum;
}

9. 删除被覆盖区间

给你一个区间列表,请你删除列表中被其他区间所覆盖的区间。
只有当 c <= a 且 b <= d 时,我们才认为区间 [a,b) 被区间 [c,d) 覆盖。在完成所有删除操作后,请你返回列表中剩余区间的数目。示例:输入:intervals = [[1,4],[3,6],[2,8]]
输出:2
解释:区间 [3,6] 被区间 [2,8] 覆盖,所以它被删除了。
public int removeCoveredIntervals(int[][] intervals) {
    // 排序生成扫描线
    Arrays.sort(intervals , (a,b)-> {
        if (a[0]==b[0]) return b[1]-a[1];
        return a[0]-b[0];
    });
    int sum = 0;
    int cur = 0;
    // 扫描:下一个区间结尾大的保留,否则被删除
    for (int[] interval : intervals) {
        if (cur < interval[1]){
            sum++;
            cur = interval[1];
        }
    }
    return sum;
}

10. 将数据流变为多个不相交区间

给你一个由非负整数 a1, a2, ..., an 组成的数据流输入,请你将到目前为止看到的数字总结为不相交的区间列表。
实现 SummaryRanges 类:
SummaryRanges() 使用一个空数据流初始化对象。
void addNum(int val) 向数据流中加入整数 val 。
int[][] getIntervals() 以不相交区间 [starti, endi] 的列表形式返回对数据流中整数的总结

示例1:
输入:
["SummaryRanges", "addNum", "getIntervals", "addNum", "getIntervals", "addNum", "getIntervals", "addNum", "getIntervals", "addNum", "getIntervals"]
[[], [1], [], [3], [], [7], [], [2], [], [6], []]
输出:
[null, null, [[1, 1]], null, [[1, 1], [3, 3]], null, [[1, 1], [3, 3], [7, 7]], null, [[1, 3], [7, 7]], null, [[1, 3], [6, 7]]]

解释:
SummaryRanges summaryRanges = new SummaryRanges();
summaryRanges.addNum(1);      // arr = [1]
summaryRanges.getIntervals(); // 返回 [[1, 1]]
summaryRanges.addNum(3);      // arr = [1, 3]
summaryRanges.getIntervals(); // 返回 [[1, 1], [3, 3]]
summaryRanges.addNum(7);      // arr = [1, 3, 7]
summaryRanges.getIntervals(); // 返回 [[1, 1], [3, 3], [7, 7]]
summaryRanges.addNum(2);      // arr = [1, 2, 3, 7]
summaryRanges.getIntervals(); // 返回 [[1, 3], [7, 7]]
summaryRanges.addNum(6);      // arr = [1, 2, 3, 6, 7]
summaryRanges.getIntervals(); // 返回 [[1, 3], [6, 7]]
class SummaryRanges {
        TreeSet<int[]> intervals;

        public SummaryRanges() {
            intervals = new TreeSet<>((a, b) -> {
                if (a[0] == b[0]) return a[1] - b[1];
                return a[0] - b[0];
            });
        }

        public void addNum(int value) {
            int[] interval = new int[]{value, value};
            // 情况1:已经有了
            if (intervals.contains(interval)) return;
            else {
                // 情况2: interval与两边相邻,则进行合并
                int[] lower = intervals.lower(interval);
                int[] higher = intervals.higher(interval);
                if (lower != null && lower[1] + 1 >= value && higher != null && value + 1 >= higher[0]) {
                    lower[1] = higher[1];
                    intervals.remove(higher);
                } else if (lower != null && lower[1] + 1 >= value) { // 情况3:和左边相连
                    lower[1] = Math.max(lower[1], value); // 易错:value可能在lower的中间
                } else if (higher != null && value + 1 >= higher[0]) {    // 情况4:和右边相连
                    higher[0] = value;
                } else {
                    intervals.add(interval);    // 和谁都不挨着
                }
            }
        }

        public int[][] getIntervals() {
            List<int[]> list = new ArrayList<>();
            for (int[] interval : intervals) {
                list.add(interval);
            }
            return list.toArray(new int[list.size()][0]);
        }
}

11. 安排会议日程

你是一名行政助理, 手里有两位客户的空闲时间表:slots1和slots,以及会议的预计持续时间duration,请你为他们安排合适的会议时间.
会议时间是两位客户都有空参加,并且持续时间能够满足预计时间duration的最早的时间间隔.
如果没有满足要求的会议时间, 就请返回一个空数组.
空闲时间」的格式是[start, end], 由开始时间start和结束时间end组成,表示从start开始, 到end结束.
题目保证数据有效: 同一个人的空闲时间不会出现交叠的情况, 也就是说, 对于同一个人的两个空闲时间
[start1, end1]和[start2, end2], 要么start1 > end2, 要么start2 > end1

示例1:
输入: slots1 = [[10,50],[60,120],[140,210]], slots2 = [[0,15],[60,70]], duration = 8  
输出:[60,68]
public List<Integer> minAvailableDuration(int[][] slots1, int[][] slots2, int duration) {
    List<Integer> list = new ArrayList<>();
    Arrays.sort(slots1, (a, b) -> a[0] - b[0]);
    Arrays.sort(slots2, (a, b) -> a[0] - b[0]);
    int p = 0;
    int q = 0;
    while (p < slots1.length && 1 < slots2.length) {
        int left = Math.max(slots1[p][0], slots2[q][0]);
        int right = Math.min(slots1[p][1], slots2[q][1]);
        if (right - left >= duration){
            list.add(left);
            list.add(left+duration);
            return list;
        }else if (slots1[p][1] < slots2[q][1]){
            p++;
        }else {
            q++;
        }
    }
    return list;
}

12. 区间列表的交集

给定两个由一些闭区间组成的列表,firstListsecondList,其中firstList[i] = [starti, endi]secondList[j]=[startj,endj].每个区间列表都是成对不相交的,并且已经排序
返回这两个区间列表的交集.
形式上,闭区间[a, b](其中a<= b)表示实数x的集合,而 a <= x <= b.
两个闭区间的交集是一组实数,要么为空集,要么为闭区间.例如,[1, 3][2, 4]的交集为[2,3] 

示例1:
输入:firstList = [[0,2],[5,10],[13,23],[24,25]], 
secondList = [[1,5],[8,12],[15,24],[25,26]]
输出:[[1,2],[5,5],[8,10],[15,23],[24,24],[25,25]]
public int[][] intervalIntersection(int[][] firstList, int[][] secondList) {
    List<int[]> list = new ArrayList<>();
    Arrays.sort(firstList, (a, b) -> a[0] - b[0]);
    Arrays.sort(secondList, (a, b) -> a[0] - b[0]);
    int firstIdx = 0;
    int secondIdx = 0;
    while (firstIdx < firstList.length && secondIdx < secondList.length) {
        if (firstList[firstIdx][0] <= secondList[secondIdx][1] && firstList[firstIdx][1] >= secondList[secondIdx][0]) {
            int[] arr = new int[2];
            arr[0] = Math.max(firstList[firstIdx][0], secondList[secondIdx][0]);
            arr[1] = Math.min(firstList[firstIdx][1], secondList[secondIdx][1]);
            list.add(arr);
        }
        if (firstList[firstIdx][1] > secondList[secondIdx][1]) {
            secondIdx++;
        } else {
            firstIdx++;
        }


    }
    return list.toArray(new int[list.size()][0]);
}

13. 员工空闲时间

给定员工的schedule列表,表示每个员工的工作时间
每个员工都有一个非重叠的时间段 Intervals 列表,这些时间段已经排好序
返回表示所有员工的共同,正数长度的空闲时间的有限时间段的列表,同样需要排好序

示例1:
输入:schedule = [[[1,2],[5,6]],[[1,3]],[[4,10]]] 
输出:[[3,4]] 
解释:共有3个员工,并且所有共同的空间时间段是[-inf, 1], [3, 4], [10, inf]。 我们去除所有包含 inf 的时间段,因为它们不是有限的时间段
// 我的解法
public List<Interval> employeeFreeTime(int[][] schedule) {
    List<Interval> intervals = new ArrayList<>();
    for (int[] sche : schedule) {
        for (int i = 0; i < sche.length / 2; i++) {
            intervals.add(new Interval(sche[2 * i], sche[2 * i + 1]));
        }
    }
    Collections.sort(intervals, (a, b) -> {
        if (a.start == b.start) return a.end - b.end;
        else return a.start - b.start;
    });
    // 进行合并
    List<Interval> list = new ArrayList<>();
    Interval cur = intervals.get(0);
    for (int i = 1; i < intervals.size(); i++) {
        if (cur.end >= intervals.get(i).start) {
            cur.end = Math.max(cur.end, intervals.get(i).end);
        } else {
            list.add(cur);
            cur = intervals.get(i);
        }
    }
    list.add(cur);
    List<Interval> res = new ArrayList<>();
    for (int i = 0; i < list.size() - 1; i++) {
        res.add(new Interval(list.get(i).end , list.get(i + 1).start ));
    }
    return res;
}

// 官方解法
public List<Interval> employeeFreeTime(int[][] schedule) {
    List<Interval> res = new ArrayList<>();
    PriorityQueue<Interval> pq = new PriorityQueue<>((a,b)->a.start-b.start);
    for (int[] sche : schedule) {
        for (int i = 0; i < sche.length / 2; i++) {
            pq.add(new Interval(sche[2 * i], sche[2 * i + 1]));
        }
    }
   Interval cur = pq.poll();
    while (!pq.isEmpty()){
        if (cur.end >= pq.peek().start){
            cur.end = Math.max(cur.end, pq.poll().end);
        }else {
            res.add(new Interval(cur.end , pq.peek().start));
            cur = pq.poll();
        }
    }
    return res;
}

14. 天际线问题(压轴)

城市的天际线是从远处观看该城市中所有建筑物形成的轮廓的外部轮廓.
给你所有建筑物的位置和高度,请返回由这些建筑物形成的天际线 。

每个建筑物的几何信息由数组 buildings 表示,其中三元组 
buildings[i] = [lefti, righti, heighti] 表示:

lefti 是第 i 座建筑物左边缘的 x 坐标。
righti 是第 i 座建筑物右边缘的 x 坐标。
heighti 是第 i 座建筑物的高度。
你可以假设所有的建筑都是完美的长方形,在高度为0的绝对平坦的表面上。

天际线应该表示为由“关键点”组成的列表,格式 [[x1,y1],[x2,y2],...],并按 x 坐标进行排序.
关键点是水平线段的左端点.列表中最后一个点是最右侧建筑物的终点,y坐标始终为0,仅用于标记天际线的终点.
此外,任何两个相邻建筑物之间的地面都应被视为天际线轮廓的一部分

注意:输出天际线中不得有连续的相同高度的水平线
例如 [...[2 3], [4 5], [7 5], [11 5], [12 7]...]是不正确的答案;
三条高度为5的线应该在最终输出中合并为一个:[...[2 3], [4 5], [12 7], ...]
image.png
class Solution {
    public List<List<Integer>> getSkyline(int[][] buildings) {
        List<List<Integer>> res = new ArrayList<>();
        List<int[]> height = new ArrayList<>();
        for (int[] build : buildings) {
            // 负号,作用1是排序先被访问,作用2是标识该位置要计数,向数飞机该点起飞了build[2]架飞机
            height.add(new int[]{build[0], -build[2]});
            // 这里就是这个正方形走完了,类似飞机降落,这些点不处理
            height.add(new int[]{build[1], build[2]});
        }
        Collections.sort(height, (a, b) -> {
            if (a[0] == b[0]) return a[1] - b[1];
            else return a[0] - b[0];
        });
        // 房子从高到地排序
        PriorityQueue<Integer> pq = new PriorityQueue<>((a, b) -> b - a);
        pq.offer(0);
        int preMax = 0;
        for (int[] h : height) {
            if (h[1] < 0) pq.add(-h[1]);   // 房子开始
            else pq.remove(h[1]);       // 房子落下

            int curMax = pq.peek();
            if (curMax != preMax) {
                List list = new ArrayList<Integer>();
                list.add(h[0]);
                list.add(curMax);
                res.add(list);
                preMax = curMax;
            }
        }
        return res;
    }
}

总结

image.png