代码随想录第三十六天 | 贪心 重叠区间问题:非交叉区间的最大个数/交叉区间最小个数(435);找重叠区间的起始 / 结束位置(763);合并重叠区间(56)

1、非交叉区间的最大个数/交叉区间最小个数

1.1 leetcode 435:无重叠区间

第一遍代码思路错

第一遍代码思路:按每个集合的开始元素从小到大排序,对于有相同的开始元素的集合,把集合大小 小排在前面。因为集合大小更小的集合不容易跟后面产生重叠
对于后面加进来的元素,通过判断起始位置和之前终点的位置关系,如果不重合更新终点位置重合那么计数的变量**+1**,其他啥也不干,相当于删除

latestEnd 初始化的时候应该是比能输入的最小值还小

class Solution {
public:
//按每个集合的开始元素从小到大排序,对于有相同的开始元素的集合,把集合大小小的排在前面
//因为集合大小更小的集合更不容易跟后面产生重叠,对于后面加进来的元素,通过判断起始位置
//和之前终点的位置关系,如果不重合更新终点位置,重合那么计数的变量+1,其他啥也不干,相当于删除
    static bool cmp(vector<int>& a, vector<int>& b) {
        if(a[0] == b[0]) return a[1] < b[1];
        return a[0] < b[0];
    }
    int eraseOverlapIntervals(vector<vector<int>>& intervals) {
        int res = 0;
        int latestEnd = -50001;//初始化的时候应该是比能输入的最小值还小
        sort(intervals.begin(), intervals.end(), cmp);
        for(int i = 0; i < intervals.size(); i++) {
            if(intervals[i][0] >= latestEnd) {
                latestEnd = intervals[i][1];
            }
            else {
                res++;
            }
        }
        return res;
    }
};

这个逻辑是错的,看这个例子
[[-52,31],[-73,-26],[82,97],[-65,-11],[-62,-49],[95,99],[58,95],[-31,49],[66,98],[-63,2],[30,47],[-40,-26]]

通过第一遍代码整出来的结果是8,但是实际结果是7

为什么呢?因为当多个数组出现重叠的时候,没有考虑清楚需要保留哪一个。上面的第一遍代码默认谁先遍历到就保留谁,也就是左边界最小的那个,但是显然是不对
要保留的那个数组需要 最小可能性与后面的数组产生重合(找最少重叠数组),那么应该找一堆重叠数组里面右边界最小的那个,他才是最不可能与后面的数组产生重合的那个,也就是一堆重合数组中应该保留

以上面的那个例子为例,进行排序后取前几个:
[-73, -26], [-65, -11], [-63, 2], [-62, -49], [-52, 31]
第一遍代码,除了保留第一个[-73, -26]外全给删了,但是如果按改进思路,对于前四个重叠的数组应该保留[-62, -49],那么第五个数组[-52, 31]就不重合了,就不用删了

class Solution {
private:
    static bool cmp(vector<int> &a, vector<int> &b) {
        return a[0] < b[0]; 
    }
public:
    int eraseOverlapIntervals(vector<vector<int>>& intervals) {
        sort(intervals.begin(), intervals.end(), cmp);
        int finalEnd = intervals[0][1];
        int count = 0;
        for (int i = 1; i < intervals.size(); i++) {
            if (intervals[i][0] < finalEnd) count++;
            else finalEnd = intervals[i][1]; // 如果没有重叠,一定要更新右边界(注意)
            if (intervals[i][1] < finalEnd) finalEnd = intervals[i][1]; // 在重叠的情况下,只更新小的边界
        }
        return count;
    }
};

根据改进思路,在重叠的情况下找里面右边界最小的那个,即每找到一个重叠的,就看看是否右边界更小更小就更新。改完后ac
逻辑更清晰的版本

class Solution {
public:
//按每个集合的开始元素从小到大排序,对于有相同的开始元素的集合,把集合大小小的排在前面
//因为集合大小更小的集合更不容易跟后面产生重叠,对于后面加进来的元素,通过判断起始位置
//和之前终点的位置关系,如果不重合更新终点位置,重合那么计数的变量+1,其他啥也不干,相当于删除
    static bool cmp(vector<int>& a, vector<int>& b) {
        if(a[0] == b[0]) return a[1] < b[1];
        return a[0] < b[0];
    }
    int eraseOverlapIntervals(vector<vector<int>>& intervals) {
        int res = 0;
        int latestEnd = -50001;//初始化的时候应该是比能输入的最小值还小
        sort(intervals.begin(), intervals.end(), cmp);
        for(int i = 0; i < intervals.size(); i++) {
            if(intervals[i][0] >= latestEnd) {
                latestEnd = intervals[i][1];
            }
            else {
                res++;
                latestEnd = min(latestEnd, intervals[i][1]);//找一堆重叠数组里面右边界最小
            }
        }
        return res;
    }
};

代码随想录对这一思路的实现
左边界排序我们就是直接求 重叠的区间,count为记录重叠区间数

对于有相同的开始元素的集合不需要集合大小 小排在前面,这样做也是没有道理的,并不是说相同的最小开始元素的集合里面挑 集合大小小 的就是一堆重叠数组里面留下来的,而是右边界最靠前

class Solution {
public:
    static bool cmp (const vector<int>& a, const vector<int>& b) {
        return a[0] < b[0]; // 改为左边界排序
    }
    int eraseOverlapIntervals(vector<vector<int>>& intervals) {
        if (intervals.size() == 0) return 0;
        sort(intervals.begin(), intervals.end(), cmp);
        int count = 0; // 注意这里从0开始,因为是记录重叠区间
        int end = intervals[0][1]; // 记录区间分割点
        for (int i = 1; i < intervals.size(); i++) {   
            if (intervals[i][0] >= end)  end = intervals[i][1]; // 无重叠的情况
            else { // 重叠情况 
                end = min(end, intervals[i][1]);
                count++;
            }
        }
        return count;
    }
};

其实代码还可以精简一下, 用 intervals[i][1] 替代 end变量只判断 重叠情况就好

class Solution {
public:
    static bool cmp (const vector<int>& a, const vector<int>& b) {
        return a[0] < b[0]; // 改为左边界排序
    }
    int eraseOverlapIntervals(vector<vector<int>>& intervals) {
        if (intervals.size() == 0) return 0;
        sort(intervals.begin(), intervals.end(), cmp);
        int count = 0; // 注意这里从0开始,因为是记录重叠区间
        for (int i = 1; i < intervals.size(); i++) {
            if (intervals[i][0] < intervals[i - 1][1]) { //重叠情况
                intervals[i][1] = min(intervals[i - 1][1], intervals[i][1]);
                count++;
            }
        }
        return count;
    }
};

思路
相信很多同学看到这道题目都冥冥之中感觉要排序,但是究竟是按照右边界排序,还是按照左边界排序呢?
其实都可以

右边界适合判断非重叠(非交叉),左边界适合判断重叠(交叉)

主要就是为了让区间尽可能的重叠,我来按照右边界排序从左向右统计非交叉区间的个数(取到最大值,想要非交叉的区间最多,显然应该关注右边界)。最后用区间总数 减去 非交叉区间的个数的最大值 就是需要移除的区间个数
此时问题就是要求 非交叉区间的 最大个数,这里记录非交叉区间的个数还是有技巧的,如图:
记录非交叉区间的个数的最大值
区间1,2,3,4,5,6都按照右边界排好序
当确定区间 1 和 区间2 重叠后,如何确定是否与 区间3 也重叠呢?
就是取 区间1 和 区间2 右边界的最小值,因为这个最小值之前的部分一定是 区间1 和区间2 的重合部分,如果这个最小值也触达到区间3,那么说明 区间 1,2,3都是重合的,如果大于这个最小值不和1重合了,最后留下的非重叠区间也是1(即右边界最小的那个,即最先遍历到的

接下来就是找大于区间1结束位置的区间,是从区间4开始。区间4结束之后,再找到区间6,所以一共记录非交叉区间的个数是三个
总共区间个数为6,减去非交叉区间(1,4,6三个区间)的个数的最大值3移除区间的最小数量就是3
与第一遍代码不同,他这个记录的是非交叉的区间数的最大值,而不是需要删除的区间个数。C++代码如下:

class Solution {
public:
    // 按照区间右边界排序
    static bool cmp (const vector<int>& a, const vector<int>& b) {
        return a[1] < b[1];
    }
    int eraseOverlapIntervals(vector<vector<int>>& intervals) {
        if (intervals.size() == 0) return 0;
        sort(intervals.begin(), intervals.end(), cmp);
        int count = 1; // 记录非交叉区间的个数
        int end = intervals[0][1]; // 记录区间分割点
        for (int i = 1; i < intervals.size(); i++) {
            if (end <= intervals[i][0]) {
                end = intervals[i][1];
                count++;
            }
        }
        return intervals.size() - count;
    }
};

时间复杂度:O(nlog n) ,有一个快排
空间复杂度:O(n),有一个快排,最差情况(倒序)时,需要n次递归调用。因此确实需要O(n)的栈空间

本题其实和leetcode 452:用最少数量的箭引爆气球非常像弓箭的数量相当于非交叉区间的数量,只要把弓箭那道题目代码里射爆气球的判断条件加个等号(认为[0,1][1,2]不是相邻区间),然后用总区间数减去弓箭数量 就是要移除的区间数量

class Solution {
public:
    // 按照区间左边界排序
    static bool cmp (const vector<int>& a, const vector<int>& b) {
        return a[0] < b[0]; // 左边界排序
    }
    int eraseOverlapIntervals(vector<vector<int>>& intervals) {
        if (intervals.size() == 0) return 0;
        sort(intervals.begin(), intervals.end(), cmp);

        int result = 1; // points 不为空至少需要一支箭
        for (int i = 1; i < intervals.size(); i++) {
            if (intervals[i][0] >= intervals[i - 1][1]) {
                result++; // 需要一支箭
            }
            else {  // 气球i和气球i-1挨着
                intervals[i][1] = min(intervals[i - 1][1], intervals[i][1]); // 更新重叠气球最小右边界
            }
        }
        return intervals.size() - result;
    }
};

2、找重叠区间的起始 / 结束位置

2.1 leetcode 763:划分字母区间

直接的思路:第一个字符是一定加入的,只要后面出现了跟第一个字符相同的元素,中间所有元素都属于第一个区间,后面出现这些元素 也需要划为一段

class Solution {
public:
    vector<int> partitionLabels(string s) {
        unordered_set<char> myset;
        int i = 0;
        vector<int> res;
        myset.insert(s[i]); // 每次都要把第一个元素塞进去
        while (i < s.size()) {
            int start = i; // start是起点,i是不断更新的终点+1,下一段的起点
            i++; // 下一段的起点至少是start+1
            for (int j = i; j < s.size(); j++) {
                if (myset.find(s[j]) != myset.end()) {
                    for (int ii = i; ii < j; ii++) { // 之前已经出现过的元素肯定在一个集合中,所以要把之前的元素都加入集合
                        myset.insert(s[ii]);
                    }
                    i = j + 1; // 新的终点+1, 下一段的起点
                }
            }
            myset.clear();
            myset.insert(s[i]);// 每次都要把第一个元素塞进去
            res.push_back(i - start); // i本来就是后一个元素,所以不用+1
        }
        return res;
    }
};

其实思路跟直接的思路一致

记录每个字母第一次出现以及最后一次出现的位置,就转成重叠区间问题
划分为尽可能多的片段,就可以转换为求重叠区间(左边界排序) 的开始和结束位置

上一题不同重叠区间内部 选择 终止位置的最大值作为重叠区间的结束位置,而且初始化,包括取得阶段结果后的初始化都有区别
具体转化方式以及求起始位置和终止位置的方法注释

class Solution {
public:
//记录每个字母第一次出现以及最后一次出现的位置,就转成重叠区间问题了
//要划分为尽可能多的片段,就可以转换为求重叠区间(左边界排序) 最大的个数
//跟上一题不同,重叠区间内部选择终止位置的最大值
    static bool cmp(vector<int>& a, vector<int>& b) {
        return a[0] < b[0];
    }
    vector<int> partitionLabels(string s) {
        vector<bool> isJudged(s.size(), false);//判断该位置上的字母是否被判断过了
        vector<vector<int>> qujian;//存储每个字母第一次出现和最后一次位置的数组
        for(int i = 0; i < s.size(); i++) {
            if(isJudged[i] == true) continue;
            char c = s[i];
            int start = i;
            int end = i;//左闭右闭区间
            for(int j = i; j < s.size(); j++) {
                if(s[j] == c) {
                    isJudged[j] = true;
                    end = j;
                }
            }
            vector<int> tmp(2, 0);
            tmp[0] = start+1;
            tmp[1] = end+1;
            qujian.push_back(tmp);
        }
        // for(int i = 0; i < qujian.size(); i++) {
        //     cout << s[qujian[i][0]] << " " << qujian[i][0] << " " << qujian[i][1] << endl;
        // }
        //每个字母第一次出现和最后一次位置的数组构造完毕,接下来就是找重叠区间的起始和结束位置
        //因为这个重叠区间的结尾位置与上一题不同,应该是所有区间里面结尾位置的最大值
        int end = qujian[0][1];
//因为需要跟终点位置比,所以要初始化为第一个数组的终点位置
        int start = qujian[0][0];
        vector<int> res;
        sort(qujian.begin(), qujian.end(), cmp);
        for(int i = 1; i < qujian.size(); i++) {
            if(qujian[i][0] <= end) {
                end = max(end, qujian[i][1]);
            }
            else {
                res.push_back(end - start + 1);
                start = qujian[i][0];
                end = qujian[i][1];
//因为需要跟终点位置比,所以要及时更新下一个重叠数组的终点位置
            }
        }
        res.push_back(end - start + 1);
        return res;
    }
};

代码随想录的相同思路相似实现
提供一种与leetcode 452:用最少数量的箭引爆气球、leetcode 435:无重叠区间 相似的解法
统计字符串中所有字符起始和结束位置记录这些区间(实际上也就是leetcode 435:无重叠区间 题目里的输入),将区间按左边界从小到大排序找到边界区间划分成组互不重叠找到的边界就是答案

主要跟第一遍代码的区别是:记录每个字母出现的区间部分使用hash表实现,因为一共就26个字母,即 countLabels() 函数的部分

class Solution {
public:
    static bool cmp(vector<int> &a, vector<int> &b) {
        return a[0] < b[0];
    }
    // 记录每个字母出现的区间
    vector<vector<int>> countLabels(string s) {
        vector<vector<int>> hash(26, vector<int>(2, INT_MIN));
        vector<vector<int>> hash_filter;
        for (int i = 0; i < s.size(); ++i) {
            if (hash[s[i] - 'a'][0] == INT_MIN) {
                hash[s[i] - 'a'][0] = i;
            }
            hash[s[i] - 'a'][1] = i;
        }
        // 去除字符串中未出现的字母所占用区间
        for (int i = 0; i < hash.size(); ++i) {
            if (hash[i][0] != INT_MIN) {
                hash_filter.push_back(hash[i]);
            }
        }
        return hash_filter;
    }
    vector<int> partitionLabels(string s) {
        vector<int> res;
        // 这一步得到的 hash 即为无重叠区间题意中的输入样例格式:区间列表
        // 只不过现在我们要求的是区间分割点
        vector<vector<int>> hash = countLabels(s);
        // 按照左边界从小到大排序
        sort(hash.begin(), hash.end(), cmp);
        // 记录最大右边界
        int rightBoard = hash[0][1];
        int leftBoard = 0;
        for (int i = 1; i < hash.size(); ++i) {
            // 由于字符串一定能分割,因此,
            // 一旦下一区间左边界大于当前右边界,即可认为出现分割点
            if (hash[i][0] > rightBoard) {
                res.push_back(rightBoard - leftBoard + 1);
                leftBoard = hash[i][0];
            }
            rightBoard = max(rightBoard, hash[i][1]);
        }
        // 最右端
        res.push_back(rightBoard - leftBoard + 1);
        return res;
    }
};

思路
一想到分割字符串就想到了回溯,但本题其实不用回溯去暴力搜索
题目要求同一字母最多出现在一个片段中,那么如何把同一个字母的都圈在同一个区间里呢?
遍历的过程中相当于是要找每一个字母的边界,如果找到之前遍历过的所有字母的最远边界,说明这个边界就是分割点了。此时前面出现过所有字母最远也就到这个边界了(思路与第一遍代码一致)

可以分为如下两步:
1、统计每一个字符最后出现位置
2、从头遍历字符,并更新字符的最远出现下标,如果找到字符最远出现位置下标 和 当前下标 相等了,则找到了分割点
把同一个字母的都圈在同一个区间里思路
代码随想录代码中 统计每一个字符最后出现的位置 还是很巧的

class Solution {
public:
    vector<int> partitionLabels(string S) {
        int hash[27] = {0}; // i为字符,hash[i]为字符出现的最后位置
        for (int i = 0; i < S.size(); i++) { // 统计每一个字符最后出现的位置
            hash[S[i] - 'a'] = i;
        }
        vector<int> result;
        int left = 0;
        int right = 0;
        for (int i = 0; i < S.size(); i++) {
            right = max(right, hash[S[i] - 'a']); // 找到字符出现的最远边界
            if (i == right) {
                result.push_back(right - left + 1);
                left = i + 1;
            }
        }
        return result;
    }
};

时间复杂度:O(n)
空间复杂度:O(1),使用的hash数组是固定大小

2.2 leetcode 763:总结

思路部分用最远出现距离模拟了圈字符的行为,之前也使用了跟上题相似的格式,但是具体到想法 对于右边界的选择 不一样

3、合并重叠区间

3.1 leetcode 56:合并区间

第一遍代码
上题 第一遍代码后半部分找重叠区间的起始和结束位置 思路完全一致
注意排序之后再确定start和end的初值,不然下面例子就不对了
Testcase
[[1,4],[0,4]]
Answer
[[0,4]]

class Solution {
public:
//跟上题 第一遍代码后半部分找重叠区间的起始和结束位置 思路完全一致
    static bool cmp(vector<int>& a, vector<int>& b) {
        return a[0] < b[0];
    }

    vector<vector<int>> merge(vector<vector<int>>& intervals) {
        vector<vector<int>> res;
        sort(intervals.begin(), intervals.end(), cmp);
        int end = intervals[0][1];
        int start = intervals[0][0];
        /*
        注意排序之后再确定start和end的初值,不然下面例子就不对了
        Testcase
        [[1,4],[0,4]]
        Answer
        [[0,4]]
        */
        for(int i = 1; i < intervals.size(); i++) {
            if(intervals[i][0] <= end) {
                end = max(end, intervals[i][1]);
            }
            else {
                vector<int> tmp;
                tmp.push_back(start);
                tmp.push_back(end);
                res.push_back(tmp);
                start = intervals[i][0];
                end = intervals[i][1];
            }
        }
        vector<int> tmp;
        tmp.push_back(start);
        tmp.push_back(end);
        res.push_back(tmp);
        return res;
    }
};

思路
本题的本质其实还是判断重叠区间问题
leetcode 452:用最少数量的箭引爆气球 和 leetcode 435:无重叠区间 都是一个套路
这几道题都是判断区间重叠区别就是判断区间重叠后的逻辑,本题是判断区间重叠后要进行区间合并
所以一样的套路先排序,让所有的相邻区间尽可能的重叠在一起,按左边界,或者右边界排序都可以,处理逻辑稍有不同

按照左边界从小到大排序之后,如果 intervals[i][0] <= intervals[i - 1][1]intervals[i]的左边界 <= intervals[i - 1]的右边界,则一定有重叠。(本题相邻区间也算重叠,所以是**<=**)

这么说有点抽象,看图:(注意图中区间都是按照左边界排序之后了)
判断区间重叠后要进行区间合并
知道如何判断重复之后,剩下的就是合并了,如何去模拟合并区间呢?
其实就是用合并区间后左边界和右边界,作为一个新的区间加入到result数组里就可以了。如果没有合并就把原区间加入到result数组

class Solution {
private:
    static bool cmp(vector<int> &a, vector<int> &b) {
        return a[0] < b[0];
    }
public:
    vector<vector<int>> merge(vector<vector<int>>& intervals) {
        vector<vector<int>> res;
        sort(intervals.begin(), intervals.end(), cmp);
        int curEnd = intervals[0][1];
        int curStart = intervals[0][0];
        for (int i = 1; i < intervals.size(); i++) {
            if (intervals[i][0] <= curEnd) // =也算重合
                curEnd = max(curEnd, intervals[i][1]);
            else {
                vector<int> tmp = {curStart, curEnd};
                res.push_back(tmp);
                curStart = intervals[i][0];
                curEnd = intervals[i][1];
            }
        }
        res.push_back(vector<int>({curStart, curEnd})); // 最后还有一个别忘了塞进去
        return res;
    }
};

代码随想录本题的写法骚的,直接把初始结果放在result里面,修改时 直接改result里面的数据
C++代码如下:

class Solution {
public:
    vector<vector<int>> merge(vector<vector<int>>& intervals) {
        vector<vector<int>> result;
        if (intervals.size() == 0) return result; // 区间集合为空直接返回
        // 排序的参数使用了lambda表达式
        sort(intervals.begin(), intervals.end(), [](const vector<int>& a, const vector<int>& b){return a[0] < b[0];});

        // 第一个区间就可以放进结果集里,后面如果重叠,在result上直接合并
        result.push_back(intervals[0]); 

        for (int i = 1; i < intervals.size(); i++) {
            if (result.back()[1] >= intervals[i][0]) { // 发现重叠区间
                // 合并区间,只更新右边界就好,因为result.back()的左边界一定是最小值,因为我们按照左边界排序的
                result.back()[1] = max(result.back()[1], intervals[i][1]); 
            } else {
                result.push_back(intervals[i]); // 区间不重叠 
            }
        }
        return result;
    }
};

时间复杂度: O(nlogn)
空间复杂度: O(logn),排序需要的空间开销

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值