区间问题专项

352 阅读4分钟

汇总区间

题目

image.png

版本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指针的更新

合并区间

题目

image.png

版本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) 对于起点和终点都进行排序, 然后判断是否合并的逻辑达到了最优, 但是实际执行速度并没有变快, 因为多了一次排序带来了更大的复杂度.

拼接视频

题目

image.png

版本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;
        }

插入区间

题目

image.png

版本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]这个区间

无重叠区间

题目

image.png

版本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) 注意是按照终点升序排列