代码随想录训练营 day36,37 | Leetcode406 根据身高重建队列、LeetCode452 用最少数量的箭引爆气、LeetCode56 合并区间、LeetCode738 单调递增的数字

406.根据身高重建队列

这题的思想和分糖果那题的其中一个类似,当有多种条件需要考虑,那么一个一个来解决,一块思考容易顾此失彼。遇到两个维度权衡的时候,一定要先确定一个维度,再确定另一个维度。

假设候选队列为 A,已经站好队的队列为 B。从 A 里挑身高最高的人 x 出来,插入到 B.。因为 B 中每个人的身高都比 x 要高,因此 x 插入的位置,就是看 x 前面应该有多少人就行了。比如 x 前面有 5 个人,那 x 就插入到队列 B 的第 5 个位置。

一般这种数对,还涉及排序的,根据第一个元素正向排序,根据第二个元素反向排序,或者根据第一个元素反向排序,根据第二个元素正向排序,往往能够简化解题过程。

class Solution {
public:
    static bool cmp(const vector<int>& a, const vector<int>& b) {
        if (a[0] == b[0]) return a[1] < b[1];
        return a[0] > b[0];
    }
    vector<vector<int>> reconstructQueue(vector<vector<int>>& people) {
        sort(people.begin(), people.end(), cmp);
        vector<vector<int>> queue;
        for(int i = 0; i < people.size(); i++){
            int pos = people[i][1];
            queue.insert(queue.begin() + pos, people[i]);
        }
        return queue;
    }
};

由于vector的插入时间复杂度很高,可以使用链表存储进行优化

class Solution {
public:
    // 身高从大到小排(身高相同k小的站前面)
    static bool cmp(const vector<int>& a, const vector<int>& b) {
        if (a[0] == b[0]) return a[1] < b[1];
        return a[0] > b[0];
    }
    vector<vector<int>> reconstructQueue(vector<vector<int>>& people) {
        sort (people.begin(), people.end(), cmp);
        list<vector<int>> que; // list底层是链表实现,插入效率比vector高的多
        for (int i = 0; i < people.size(); i++) {
            int position = people[i][1]; // 插入到下标为position的位置
            std::list<vector<int>>::iterator it = que.begin();
            while (position--) { // 寻找在插入位置
                it++;
            }
            que.insert(it, people[i]);
        }
        return vector<vector<int>>(que.begin(), que.end());
    }
};

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

接下来的3题都是重叠区间的问题

想象中,气球是按照区间从大到小摆放的,但数据给的顺序不是,为了方便查找相邻的区间,需要将数组进行排序,使得气球的区间的顺序也是按照从大到小的顺序排列的。

这里的排序有两种方法,第一种是按start排序,排出来的上述的正常思维的顺序。

主要思路是如果两个区间完全不挨着,那肯定得多用一根箭;如果两个区间挨着就不用多加一根,但3个及以上区间挨着时就一定要注意挨着的这些区间的最小end,如果下一个区间的start大于这个最小end,那即使区间挨着也得多加一根箭。

class Solution {
public:
    static bool cmp(const vector<int>& a, const vector<int>& b){
        return a[0] < b[0];
    }
    int findMinArrowShots(vector<vector<int>>& points) {
        sort(points.begin(), points.end(), cmp);
        int num = 1;
        int lastright = points[0][1];
        for(int i = 1; i < points.size(); i++){
            if(points[i][0] > lastright){//不相交的条件:后一个气球的开始坐标大于(不能等于)前一个气球的结束坐标
                num++;
                lastright = points[i][1];
            } 
            else{//如果气球重叠了,重叠气球中右边边界的最小值 之前的区间一定需要一个弓箭。
                lastright = min(points[i][1], lastright);
            }
        }
        return num;
    }
};

另一种思路,按end排序。

如果用end排序,那么一个最大的好处就是区间列表里第一项的end是最小的end,这样就不用再上上文提到的那样去找最小end了。在end排序下,只有一种情况需要多加箭,那就是next_start > cur_end时,其他情况统统都是一支箭搞定,这样从代码上还是思路上都简化了不少。

class Solution {
public:
    static bool cmp(const vector<int>& a, const vector<int>& b){
        return a[1] < b[1];
    }
    int findMinArrowShots(vector<vector<int>>& points) {
        sort(points.begin(), points.end(), cmp);
        int num = 1;
        int lastright = points[0][1];
        for(int i = 1; i < points.size(); i++){
            if(points[i][0] > lastright){//不相交的条件:后一个气球的开始坐标大于(不能等于)前一个气球的结束坐标
                num++;
                lastright = points[i][1];
            } 
        }
        return num;
    }
};

435. 无重叠区间

思路和上一题一样,代码也相似,虽然都是更新右区间,但是含义是不相同的,要想明白这题代码里删除的到底是哪个。

class Solution {
public:
    static bool cmp(vector<int>& a, vector<int>& b){
        return a[0] < b[0];
    }
    int eraseOverlapIntervals(vector<vector<int>>& intervals) {
        sort(intervals.begin(), intervals.end(), cmp);
        int result = 0;
        for(int i = 1; i < intervals.size(); i++){
            if(intervals[i][0] < intervals[i-1][1]){
                result++;
                intervals[i][1] = min(intervals[i][1], intervals[i-1][1]);
                //因为找到需要移除区间的最小数量,所以选择删除右区间更大的,使得和下一个重叠的可能性减少
                //关键在于贪心:若有重叠,移除右边界大的那个区间,代码上体现在更新当前区间的右边界
            }
        }
        return result;
    }
};

这题的题解里找到了一个关于为什么采取右端排序更好的解释,写的很好的,记录一下

关于为什么是推荐按照区间右端点排序?
官解里对这个描述的非常清楚了,这个题其实是预定会议的一个问题,给你若干时间的会议,然后去预定会议,那么能够预定的最大的会议数量是多少?核心在于我们要找到最大不重叠区间的个数。 如果我们把本题的区间看成是会议,那么按照右端点排序,我们一定能够找到一个最先结束的会议,而这个会议一定是我们需要添加到最终结果的的首个会议。(这个不难贪心得到,因为这样能够给后面预留的时间更长)。

对于按照区间左端点排序,当两个比较的区间存在重叠时,再比较区间右端点的大小,保留右端点小的区间(对应结束时间早的区间),这样能够满足剩余非重叠区间的个数最多。

对于按照区间右端点排序,当两个比较的区间存在重叠时,无需比较右端点的大小, 因为按照右端点排序, 后者肯定大于前者,因此只需保留右端点(前者)小的区间(对应结束时间早的区间)。

综上所述:按照左区间排序比按照右区间排序多了一步比较两区间右端点大小,选出右端点小的区间的步骤。

763.划分字母区间

这题我没有采用之前区间重叠的思路,我的思路是用双指针去找每个字符最后出现的位置,然后维护一个最大的右边界值,当 i = right 的时候,说明 right 之前的所有字符已经不会出现在右边了,可以分割了。

class Solution {
public:
    vector<int> partitionLabels(string s) {
        int right = 0;
        int left = 0;
        vector<int> result;
        for(int i = 0; i < s.size(); i++){
            for(int j = s.size()-1; j > 0; j--){
                if(s[j] == s[i]){
                    right = max(j, right);
                    break;
                }
            }
            if(i == right){
                result.push_back(right+1 - left);
                left = right + 1;
                right = 0;
            }
        }
        return result;
    }
};

这个思路的问题就在于慢,每个字符都重新找了一遍右边界,但其实有很多重复的操作。

可以将里面的for循环提取出来,先找区间内所有种类字符的右边界在哪,存起来,再遍历一次找切割点。

class Solution {
public:
    vector<int> partitionLabels(string s) {
        //创建哈希表来存储我们记录到字符串中的元素的最后下标
        int hash[27] = {0};
        //遍历字符串,将最后一次出现元素记录放入哈希表中
        for(int i=0;i<s.size();i++){
            hash[s[i]-'a'] = i;
        }
        //创建结果容器,用于返回
        vector<int>ans;
        //创建左右指针
        int right = 0;
        int left = 0;
        //再次遍历字符串,确定我们的断点
        for(int j=0;j<s.size();j++){
            //找到出现最远的字符,得到它的下标
            right = max(right,hash[s[j]-'a']);
            if(right == j){
                //将结果放入容器中,别忘了字符串从是零开始的,所以结果+1
                ans.push_back(right - left + 1);
                //找到边界后移动左指针,以后续用于确定新边界
                left = j + 1;
            }
        }
        //返回
        return ans;
    }
};

  • 38
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值