目录
题目
注:这里借用英文版的案例,英文版说明清晰。
思路解析
在查询之前,我们需要知道每一对点对的连接边数。但是如果对每一种点对进行一遍扫描。那么这样的代价我们是不能承受的。所以,我们需要进行优化。关注案例得到点对的连接边数,由包含点对的点值贡献。可见案例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;
}
};