Leetcode 1782. 统计点对数目

文章讲述了在处理大量数据的点对连接边数查询问题时,如何通过二分查找、双指针技巧以及前/后缀和数组优化查询效率。作者详细介绍了利用容斥定理统计degree和cnt数组的方法,以及如何通过这些数组减少查询时间复杂度。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录

题目

思路解析

策略1:二分查找

策略2:双指针

策略3:前/后缀和数组查询


题目

 注:这里借用英文版的案例,英文版说明清晰。

思路解析

在查询之前,我们需要知道每一对点对的连接边数。但是如果对每一种点对进行一遍扫描。那么这样的代价我们是不能承受的。所以,我们需要进行优化。关注案例得到点对的连接边数,由包含点对的点值贡献。可见案例1图标理解,注意标红值。

如果我们定义 degree[i] 表示包含点i的边个数,那么根据容斥定理,我们知道 incident(a, b) = degree(a) + degree(b) - degree(a, b)。

因此,我们可以在O(edge.size())的时间内统计出degree数组。并且计算incident的时间为O(1)

至此,我们就解决了第一个难点。

还有第二个难点,如何查询。因为我们存在两种维度的degree,所以我们不妨将二维的degree改写为cnt(a,b)。我们如果要查询符合不等式 degree(a) + degree(b) - cnt(a, b) > query(i) 的点对个数。按照最基础的情况,我们为每一次的查询都进行O(N^2)的遍历就好了。但是在庞大的数据规模面前,这是显然不可行的。

所以我们需要做出改进。

策略1:二分查找

为了查询/查找,我们最先想到的应该是二分查找。但是这里有一个显著的问题:对什么二分查找?问题似乎有些难以回答,如果对 degree(a) + degree(b) - cnt(a, b) 进行查询那将毫无意义。疑问点对太多了。我们不可能枚举计算出所有的点对的信息,我们需要一种可转换的关系。再者,我们观察到,我们拥有degree数组和cnt数组。所以,我们可以将题目弱化为对 degree(a) + degree(b) > query(i) 的查找。此后,我们在提出 degree(a) + degree(b) - cnt(b) <= query(i) 的值即可。

AC代码:

class Solution {
public:
    vector<int> countPairs(int n, vector<vector<int>>& edges, vector<int>& queries) {
        vector<int> degree(n);
        unordered_map<int, int> cnt;
        for (auto edge : edges) {
            int x = edge[0] - 1, y = edge[1] - 1;
            if (x > y) {
                swap(x, y);
            }
            degree[x]++;
            degree[y]++;
            cnt[x * n + y]++;
        } // 获取degree 和 cnt 信息

        vector<int> arr = degree;
        vector<int> ans;
        sort(arr.begin(), arr.end());//为二分查找左准备
        for (int bound : queries) {
            int total = 0;
            for (int i = 0; i < n; i++) {
                int j = upper_bound(arr.begin() + i + 1, arr.end(), bound - arr[i]) - arr.begin();
                total += n - j;
            }
            for (auto &[val, freq] : cnt) {
                int x = val / n;
                int y = val % n;
                if (degree[x] + degree[y] > bound && degree[x] + degree[y] - freq <= bound) {
                    total--;
                } // 纳入弱化可行解,但不是答案可行解,则剔除
            } // 剔除多余的状态
            ans.emplace_back(total); // 反馈查询结果
        }

        return ans;
    }
};

策略2:双指针

面对 N数和 与 某一个数字的大小关系,我们都可以使用双指针优化。

所以,代码可以进行以下优化。

AC代码:

class Solution {
public:
    vector<int> countPairs(int n, vector<vector<int>>& edges, vector<int>& queries) {
        vector<int> degree(n);
        unordered_map<int, int> cnt;
        for (auto edge : edges) {
            int x = edge[0] - 1, y = edge[1] - 1;
            if (x > y) {
                swap(x, y);
            }
            degree[x]++;
            degree[y]++;
            cnt[x * n + y]++;
        }

        vector<int> arr = degree;
        vector<int> ans;
        sort(arr.begin(), arr.end());
        for (int bound : queries) {
            int total = 0;
            for (int i = 0, j = n - 1; i < n; i++) {
                while (j > i && arr[i] + arr[j] > bound) {
                    j--;
                }
                // i < j :(j, n - 1] 符合
                // j < i :(j, n - 1] 符合 但是点对只有 (i , n - 1]个
                total += n - 1 - max(i, j); 
            } // 双指针优化,注意需要统计个数,所以需要改动
            for (auto &[val, freq] : cnt) {
                int x = val / n;
                int y = val % n;
                if (degree[x] + degree[y] > bound && degree[x] + degree[y] - freq <= bound) {
                    total--;
                }
            }
            ans.emplace_back(total);
        }

        return ans;
    }
};

策略3:前/后缀和数组查询

面对 degree(a) + degree(b) 我们可以采用前缀和数组的方式,这是因为我们需要查找的是个数。也就是说,如果我们知道 frequency(degree(a)) 和 frequency(degree(b)),那么我们之就能直接计算出符合条件的式子个数。二者只需要O(m) 的预处理。

其次,我们需要的是大于它的或有情况,也就是说两个集合的度之和需要大于query(i)。也就是后缀数组。此后,我们就能计算出所有情况的点对信息。因为我们已经将它们进行了离散化。

但是,我们仍然没有做出剔除操作。而剔除操作就是将 degree(a) + degree(b) - cnt(a, b) 的个数+1,而对应的 degree(a) + degree(b) 个数 - 1。注意此时,统计的是多余的可行解个数,对其进行前缀和。

最后答案就是 符合弱化的可行解 - 弱化后多余的可行解 即为查询答案。

class Solution {
public:
    vector<int> countPairs(int n, vector<vector<int>>& edges, vector<int>& queries) {
        int m = edges.size();

        /* 计算度数 deg */
        vector<int> deg(n, 0);
        for(auto& e : edges)
            e[0]--, e[1]--, deg[e[0]]++, deg[e[1]]++;//作完偏移,再统计

        /* 计算度数 deg 的频率数组 freq。频率数组最多有 2*sqrt(m) 个非零项 */
        unordered_map<int,int> freq;
        for(int i = 0; i < n; ++i)
            freq[deg[i]]++;
        
        /* 计算 cnt1,复杂度 O(m) */
        vector<int> cnt(2*m + 1, 0);
        for(auto it = freq.begin(); it != freq.end(); ++it) {
            cnt[2*it->first] += it->second * (it->second - 1) / 2;
            for(auto it2 = next(it); it2 != freq.end(); ++it2) {
                cnt[it->first + it2->first] += it->second * it2->second;
            }
        }

        /* 计算 cnt1 的后缀和 */
        for(int i = 2*m - 1; i >= 0; --i)
            cnt[i] += cnt[i+1];

        /* 用一个哈希表统计每条边重复了多少次(这里用位运算把边压缩成整数存到哈希表里) */
        unordered_map<int, int> c;
        for(const auto& e : edges)
            c[(min(e[0], e[1]) << 16) | max(e[0], e[1])]++;

        /* 对每条边 (u, v),所有落在 [deg[u] + deg[v] - c(u, v), deg[u] + deg[v] - 1] 的查询需要排除 */
        vector<int> cnt2(2*m + 1, 0);
        for(const auto& p : c) {
            int u = p.first >> 16, v = p.first & 0xFFFF;
            cnt2[deg[u] + deg[v] - p.second]++;
            cnt2[deg[u] + deg[v]]--;
        }

        /* cnt2 是差分数组,求一次前缀和 */
        for(int i = 1; i <= m; ++i)
            cnt2[i] += cnt2[i-1];

        vector<int> ret;
        for(const auto& q : queries)
            ret.push_back(cnt[q+1] - cnt2[q]);
        
        return ret;
    }
};

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值