题目描述

给你一个整数 n ,表示服务器的总数目,再给你一个下标从 0 开始的 二维 整数数组 logs ,其中 logs[i] = [server_id, time] 表示 id 为 server_id 的服务器在 time 时收到了一个请求。

同时给你一个整数 x 和一个下标从 0 开始的整数数组 queries 。

请你返回一个长度等于 queries.length 的数组 arr ,其中 arr[i] 表示在时间区间 [queries[i] - x, queries[i]] 内没有收到请求的服务器数目。

注意时间区间是个闭区间。

示例 1:

输入:n = 3, logs = [[1,3],[2,6],[1,5]], x = 5, queries = [10,11]
输出:[1,2]
解释:
对于 queries[0]:id 为 1 和 2 的服务器在区间 [5, 10] 内收到了请求,所以只有服务器 3 没有收到请求。
对于 queries[1]:id 为 2 的服务器在区间 [6,11] 内收到了请求,所以 id 为 1 和 3 的服务器在这个时间段内没有收到请求。

示例 2:

输入:n = 3, logs = [[2,4],[2,1],[1,2],[3,1]], x = 2, queries = [3,4]
输出:[0,1]
解释:
对于 queries[0]:区间 [1, 3] 内所有服务器都收到了请求。
对于 queries[1]:只有 id 为 3 的服务器在区间 [2,4] 内没有收到请求。

提示:

  • 1 <= n <= 10^5

  • 1 <= logs.length <= 10^5

  • 1 <= queries.length <= 10^5

  • logs[i].length == 2

  • 1 <= logs[i][0] <= n

  • 1 <= logs[i][1] <= 10^6

  • 1 <= x <= 10^5

  • x < queries[i] <= 10^6


思路

一开始误以为只有 logs 里面访问过的服务器才生效,其实没列出来的也算数。题目说 n 个服务器,然后给出一些访问列表。

先说下,我第一次拿到手的思路:

  1. 先建立一个 map,key 为时间点,值为在这个时间点访问了哪些 server。
  2. 遍历 queries,用两次二分确定当前区间覆盖哪些时间点
  3. 统计这些时间点访问过的 servers
  4. 用 n 减去访问过的 servers 数量记为当前 query 的结果

但这样做超时了。然后看了提示说用双指针,滑动窗口?想了半天没想到怎么滑。看完题解:滑动窗口还能这么用?

具体做法是,先将 logs 按时间升序排列,问题转化为定长(x + 1)的滑动窗口问题。因为要按照时间先后与顺序滑动,所以 queries 也要按时间排序。但答案和 queries 的顺序强相关,所以不能直接排序,那就排它的索引。从上一次询问,到这一次询问 q 的过程中,

  • 把 的新增请求进入窗口(右边界)
  • 把 的请求离开窗口(左边界)

同时维护每个服务器收到了多少个请求 cnt,以及没有收到请求的服务器数目 not_received(初值为 n):

  • 若服务器 i 收到的请求进入窗口,则把 cnt[i] 加一,如果加一前等于 0,那么把 not_received 减少一
  • 若服务器 i 收到的请求离开窗口,则把 cnt[i] 减一,如果减一后等于 0,那么把 not_received 增加一
vector<int> countServers(int n, vector<vector<int>>& logs, int x, vector<int>& queries) {
    // logs按时间点升序排列
    sort(logs.begin(), logs.end(), [](const vector<int>& a, const vector<int>&b){
        return a[1] < b[1];
    });
    vector<int> idx(queries.size());
    iota(idx.begin(), idx.end(), 0);
    sort(idx.begin(), idx.end(), [&](int a, int b){ // 排queries的索引
        return queries[a] < queries[b];
    });
    vector<int> cnt(n + 1, 0); // 窗口内每个服务器收到的请求次数,从用例看出server编号从1开始
    int not_received = n; // 记录未收到请求的服务器数目
    int left = 0, right = 0;
    for (int qid : idx) {
        int& q = queries[qid];
        while (right < logs.size() && logs[right][1] <= q) { // 滑动右边界
            int server = logs[right][0];
            if (cnt[server]++ == 0) { // server第一次被访问,未被访问的server就减少1
                --not_received;
            }
            ++right;
        }
        while (left < logs.size() && logs[left][1] < q - x) { // 滑动左边界
            int server = logs[left][0];
            if (--cnt[server] == 0) { // server被访问次数降到0,未被访问的server就加1
                ++not_received;
            }
            ++left;
        }
        q = not_received; // 原地修改queries,因为后面用不到了
    }
    return queries;
}

这个题精妙之处在于它的滑窗目标,在哪个数组上面滑不是很明显。它相当于是每次固定一个窗口,在 logs 数组中定界。下一次 query 又换一个窗口,然后我们还得在 logs 数组中滑动至目标窗口。因为滑窗你得一步一步滑,一步一步处理和维护信息,才能边滑边获得你想要的信息。如果直接跳,那就无法处理全部的信息,最后也得不到正确答案。就像这题,先按时间维度排序,把两个数组都变成单向移动的,这样才能套用滑窗。试想一下,如果 queries 不按时间先后排序,有时先问你后面的,有时再问你前面的。这样你滑窗的过程中还得回头处理以前窗口的信息,就复杂了。

References

  1. 灵神题解 - 离线+滑动窗口