题目描述
难度分:2600
输入n(1≤n≤2×105),m(1≤m≤3×105),q(1≤q≤5×105)和一个1~n的排列p。
有一个n个点m条边的图,点v的点权为p[v]。然后输入m条边。节点编号从1开始,不含重边和自环。
然后输入q个询问,格式如下:
1 v
:输出v所在连通块的最大点权,然后把这个点权改成0。2 i
:删除输入的第i条边。i从1开始。保证i之前没有被删除过。
输入样例
5 4 6
1 2 5 4 3
1 2
2 3
1 3
4 5
1 1
2 1
2 3
1 1
1 2
1 2
输出样例
5
1
2
0
算法
启发式合并
这个题刚一看到就想反着来做,这样删边就变成了加边,用并查集维护非常方便。但问题就在于操作1的点权赋0操作,这个操作我想了很久都没想到怎么高效地倒着操作,所以再次宣告了周五开茶失败。看了一下题解区,大部分是DFS序+线段树,有个基本考点就是“重构树”,这玩意儿没学过,我也确实想不到。不过除了这种解法,还看到有一个倒着做启发式合并的思路我是想过的,只是没有考虑全导致没能开出来,所以着重学习了这个解法。
构建一个映射group,它表示“节点 → 这个节点所在连通块的全部点权”。遍历边集,忽略掉所有待删除的边,利用启发式合并构建group映射,形成整棵树(可能是森林)。然后按照以下步骤来做:
- 倒着处理每个询问,当遇到删边操作时(操作2),利用启发式合并更新group映射,把这条边加入到森林中。
- 遍历i∈[1,n],把group[i]中的点权排好序。并构建一个root数组,root[a[i]]=find(i),表示点权a[i]属于find(i)这个连通分量(find是并查集操作)。进行这一步就是为了高效查询最大那个待归零的点权。
- 最后正着处理每个询问,如果是操作1,找到i所在连通分量中最大的未归零点权,将其标记used[group[x].back()]=
true
,并从group[x]中弹出这个点权。如果是操作2,就对group进行启发式合并,同时维护root数组和并查集的father数组p。
复杂度分析
时间复杂度
启发式合并每次都会使得集合的大小至少变为原来的两倍,那么最差情况下就是从1变成n,需要进行O(log2n)这个级别的合并次数。因此遍历边集时的启发式合并时间复杂度为O(mlog2n);倒序处理询问时的启发式合并时间复杂度为O(qlog2n);对每个group[x]排序,由于排序的元素个数在O(nlog2n)级别,因此这个步骤的时间复杂度为O(n(log2n)2)。最后再正序处理每个询问,对于每个询问都要进行O(log2n)级别的操作,时间复杂度为O(qlog2n)。
综上,整个算法的时间复杂度为O((m+q)log2n+n(log2n)2)
空间复杂度
并查集的p数组,sz数组都是O(n)级别的。边集标记数组del是O(m),点集边集数组used是O(n)。点权所属连通块的数组root空间是O(n)。最大的瓶颈是映射group,由于元素数量是O(nlog2n),所以空间复杂度是O(nlog2n)。
综上,整个算法的额外空间复杂度为O(n+m+nlog2n)。
C++ 代码
#include <bits/stdc++.h>
using namespace std;
const int N = 200010, M = 300010;
int n, m, q, a[N], p[N], sz[N], root[N];
bool del[M], used[N];
vector<int> group[N];
struct Edge {
int x, y;
} edges[M];
struct Query {
int opt, id, rx, ry;
} queries[N + M];
int find(int x) {
return p[x] == x? x: find(p[x]);
}
int query(int x){
while(group[x].size() && (root[group[x].back()] != x || used[group[x].back()])) group[x].pop_back();
if(group[x].empty()) return 0;
int w = group[x].back();
return group[x].pop_back(), used[w] = 1, w;
}
int main() {
scanf("%d%d%d", &n, &m, &q);
for(int i = 1, x; i <= n; i++) {
scanf("%d", &x);
group[p[i]=i].push_back(a[i]=x);
sz[i] = 1;
}
for(int i = 1; i <= m; i++) {
scanf("%d%d", &edges[i].x, &edges[i].y);
}
for(int i = 1; i <= q; i++){
scanf("%d%d", &queries[i].opt, &queries[i].id);
if(queries[i].opt == 2) {
del[queries[i].id] = 1;
}
}
for(int i = 1; i <= m; i++){
if(del[i]) continue;
int x = find(edges[i].x), y = find(edges[i].y);
if(x == y) continue;
if(sz[x] > sz[y]) swap(x, y);
for(int w: group[x]) {
group[y].push_back(w);
}
p[x] = y, sz[y] += sz[x];
}
for(int i = q; i >= 1; i--){
int x = edges[queries[i].id].x, y = edges[queries[i].id].y;
if(queries[i].opt == 1 || find(x) == find(y)) continue;
x = find(x), y = find(y);
if(sz[x] > sz[y]) swap(x, y);
queries[i].rx = x;
queries[i].ry = y;
p[x] = y;
sz[y] += sz[x];
for(int w: group[x]) {
group[y].push_back(w);
}
}
for(int i = 1; i <= n; i++) {
sort(group[i].begin(), group[i].end());
root[a[i]] = find(i);
}
for(int i = 1; i <= q; i++){
if(queries[i].opt == 1) {
printf("%d\n", query(find(queries[i].id)));
}else if(queries[i].rx){
for(int j: group[queries[i].rx]) {
root[j] = queries[i].rx;
}
p[queries[i].rx] = queries[i].rx;
}
}
return 0;
}