【C/C++】2251. 花期内花的数目

369 阅读6分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第5天,点击查看活动详情


题目链接:2251. 花期内花的数目

题目描述

给你一个下标从 0 开始的二维整数数组 flowers ,其中 flowers[i] = [starti, endi] 表示第 i 朵花的 花期 从 startistart_i 到 endiend_i (都 包含)。同时给你一个下标从 0 开始大小为 n 的整数数组 persons ,persons[i] 是第 i 个人来看花的时间。

请你返回一个大小为 n 的整数数组 answer ,其中 answer[i] 是第 i 个人到达时在花期内花的 数目 。

提示:

  • 1flowers.length5×1041 \leqslant flowers.length \leqslant 5 \times 10^4
  • flowers[i].length==2flowers[i].length == 2
  • 1startiendi1091 \leqslant start_i \leqslant end_i \leqslant 10^9
  • 1persons.length5×1041 \leqslant persons.length \leqslant 5 \times 10^4
  • 1persons[i]1091 \leqslant persons[i] \leqslant 10^9

示例 1:

ex1new.jpg

输入:flowers = [[1,6],[3,7],[9,12],[4,13]], persons = [2,3,7,11]
输出:[1,2,2,2]
解释:上图展示了每朵花的花期时间,和每个人的到达时间。
对每个人,我们返回他们到达时在花期内花的数目。

示例 2:

ex2new.jpg

输入:flowers = [[1,10],[3,3]], persons = [3,3,2]
输出:[2,2,1]
解释:上图展示了每朵花的花期时间,和每个人的到达时间。
对每个人,我们返回他们到达时在花期内花的数目。

整理题意

题目给了每朵花 开花的时间节点花落的时间节点,以及每个人 到达的时间节点,问每个人在到达的时候可以看到多少朵花是开着的。

解题思路分析

习惯性动作,首先观察题目数据范围:

  • 最多 5×1045\times10^4 朵花,也就是花开花落的时间节点最多 10510^5 个。
  • 时间节点的值在 10910^9 以内,数据范围很大,但时间节点个数最多 10510^5 个,所以可以对时间节点值进行离散化处理。

离散化:当我们只关心数据之间的大小关系,不关心数据具体的值为多少时,就可以对数据进行离散化处理,将大范围的数据离散化缩小到较小的范围中去。这里时间节点值的范围高达 10910^9 ,如果我们需要以数组下标来表示时间节点,对于这个数据范围是无法开这么大的数组的,但我们知道这里时间节点个数最多 10510^5 个,也就是最多只需要开到 10510^5 大小的数组就可以存下所有时间节点了。同时我们只关心时间节点的大小关系,满足离散化要求。

方法一:离散化 + 差分数组

首先我们可以对时间节点值进行离散化处理,将 10910^9 的数据映射到 10510^5 里。然后建立差分数组 diff[i] 来记录时间节点为 i 的开花数量,具体操作为:

  1. 遍历每一朵花的开花时间节点 st 和花落时间节点 ed
  2. 在花开时间节点处将差分数组增加一朵花:diff[st]++
  3. 在花落时间节点的下一个时间节点处将差分数组减少一朵花:diff[ed + 1]--
  4. 最后得差分数组的前缀和即可得每个时间节点 i 的花开数量;
  5. 遍历每个人到达的时间节点 persons[i],该时间节点开花的数目即为 diff[i] 。

方法二:有序哈希表 + 差分数组

除了数据离散化这里还可以使用有序哈希表进行存储,注意是有序哈希表 map,无序的 unordered_map 无法实现从小到大遍历时间节点来统计当前花开数量。

因为是哈希表存储,省去了离散化操作,可直接对时间节点进行操作,最后是对 map 集合进行遍历,因为有序,所以在遍历的时候可以直接累加统计时间节点的开花数量。

方法三:排序

方法一和方法二的核心操作都离不开 排序 ,我们这里可以将花开、花落和人到达的时间都看成每个时间节点,对这些时间节点进行排序即可,最后从小到大遍历一遍即可。具体操作为:

  1. (st[i], -INF) 看成第 i 朵花开的时间节点;
  2. (persons[i], i) 看成第 i 个人到达的时间节点;
  3. (ed[i], INF) 看成第 i 朵花凋谢的时间节点。 需要 注意 的是:为什么要将时间节点带上 -INFiINF,这是因为排序的时候当时间节点相同的时候,按照 花开 -INF,人 i ,花落 INF 的顺序处理。

先按照第一个参数时间节点排序,当时间节点相同时按照第二个参数排序,最后从小到大遍历时间节点,同时维护变量 now 表示当前开花数量即可。

方法四:排序 + 二分查找

  1. 将花开和花落时间节点分开单独排序;
  2. 用每个人到达的时间节点分别在两个有序数组中二分查找当前到达时间节点前有多少朵花开了(包括到达时间),有多少朵花凋谢了(不包括到达时间),相减后得到答案。

虽然描述简单,但其中还有很多 细节 需要 注意

  • 因为数组下标从 0 开始;
  • 在二分查找花开的数量时是 包括 当前到达时间节点的,所以需要查找 第一个大于 当前到达时间节点的下标。这样得到的下标减去下标 0 后得到的就是包括当前到达时间节点的开花数量。
  • 在二分查找花落的数量时是 不包括 当前到达时间节点的,所以需要查找 第一个大于等于 当前到达时间节点的下标。这样得到的下标减去下标 0 后得到的就是不包括当前到达时间节点的花落数量。

复杂度分析

  • 时间复杂度:O(nlogn)O(n \log n),其中 n 为所有时间节点个数的总和(包括花开、花落和人),四种方法核心思路都离不开排序,所以总的时间复杂度都在 O(nlogn)O(n \log n) 级别。
  • 空间复杂度:O(n)O(n),其中 n 为所有时间节点个数的总和(包括花开、花落和人),仅需存储所有时间节点的空间即可。

代码实现

这里仅推荐方法二、三、四的做法,方法一的做法过于繁琐,代码冗长,性价比很低。所以仅展示方法二、三、四的代码实现。

方法二:有序哈希表 + 差分数组

class Solution {
public:
    vector<int> fullBloomFlowers(vector<vector<int>>& flowers, vector<int>& persons) {
        //因为数据范围1e9所以用map,因为需要有序遍历,所以不使用unordered_map
        map<int, int> diff;
        diff.clear();
        int n = flowers.size();
        //差分记录花开花落
        for(int i = 0; i < n; i++){
            diff[flowers[i][0]]++;
            //注意花落时间要+1
            diff[flowers[i][1] + 1]--;
        }
        //保存每个人的id
        vector<pair<int, int>> vec;
        vec.clear();
        int m = persons.size();
        for(int i = 0; i < m; i++){
            vec.push_back(make_pair(persons[i], i));
        }
        //对人到达的时间进行排序
        sort(vec.begin(), vec.end(), [&](pair<int, int> a, pair<int, int> b)->bool{
            return a.first < b.first;
        });
        //now 记录当前花开数量
        int now = 0;
        auto iter = diff.begin();
        for(int i = 0; i < m; i++){
            //累加到第i个人到达的时间位置
            while(iter != diff.end() && iter->first <= vec[i].first){
                now += iter->second;
                iter++;
            }
            //更新对应id的人所看见的花数量
            persons[vec[i].second] = now;
        }
        return persons;
    }
};

方法三:排序

class Solution {
public:
    vector<int> fullBloomFlowers(vector<vector<int>>& flowers, vector<int>& persons) {
        vector<pair<int, int>> vec;
        vec.clear();
        int INF = 1e9;
        int n = flowers.size();
        //放入花开花落时间节点
        for(int i = 0; i < n; i++){
            vec.push_back(make_pair(flowers[i][0], -INF));
            vec.push_back(make_pair(flowers[i][1], INF));
        }
        int m = persons.size();
        //放入人到达的时间节点
        for(int i = 0; i < m; i++){
            vec.push_back(make_pair(persons[i], i));
        }
        //自定义排序
        sort(vec.begin(), vec.end(), [&](pair<int, int> a, pair<int, int> b){
            if(a.first == b.first) return a.second < b.second;
            return a.first < b.first;
        });
        //now 记录当前花开数量
        int now = 0;
        int k = vec.size();
        //从小到大遍历时间节点 维护now值即可
        for(int i = 0; i < k; i++){
            if(vec[i].second == -INF) now++;
            else if(vec[i].second == INF) now--;
            else{
                persons[vec[i].second] = now;
            }
        }
        return persons;
    }
};

方法四:排序 + 二分查找

class Solution {
public:
    vector<int> fullBloomFlowers(vector<vector<int>>& flowers, vector<int>& persons) {
        vector<int> st, ed;
        st.clear(); ed.clear();
        //记录花开花落时间
        int n = flowers.size();
        for(int i = 0; i < n; i++){
            st.push_back(flowers[i][0]);
            ed.push_back(flowers[i][1]);
        }
        //对花开花落时间进行排序
        sort(st.begin(), st.end());
        sort(ed.begin(), ed.end());
        int m = persons.size();
        //对于每个人到达的时间进行二分查找有多少花在当前时间节点之前开(包括当前时间节点)
        //减去当前时间节点之前花落数(不包括当前时间节点)
        for(int i = 0; i < m; i++){
            //因为下标从零开始的缘故,开花数需要使用upper_bound()寻找,花落数需要用lower_bound()寻找
            //保证减去st.begin()后得到的下标值为 "包括当前时间节点" 的开花数
            int stFlowers = upper_bound(st.begin(), st.end(), persons[i]) - st.begin();
            //保证减去ed.begin()后得到的下标值为 "不包括当前时间节点" 的落花数
            int edFlowers = lower_bound(ed.begin(), ed.end(), persons[i]) - ed.begin();
            persons[i] = stFlowers - edFlowers;
        }
        return persons;
    }
};

总结

该题解法的 核心思路在于对时间节点进行排序

  • 很容易想到方法一的做法,但实际操作起来比较繁琐,代码冗长,检查代码较为困难;
  • 方法二是较为常规的做法。
  • 方法三在思想和技巧上都很巧妙,利用 INF 作为第二排序参数来规避相同时间节点的处理顺序问题。
  • 方法四是在思想上非常巧妙,巧妙利用简单的二分来实现对花开花落的查找即可,但是在处理过程中有较多的下标细节需要注意,很容易出错,不过代码简洁,查代码方便。

结束语

不要在该努力的时候选择安逸,也别在最好的青春年华选择懈怠。奋斗是青春最亮丽的底色,对生活充满希望的人,永远正值青春。