题目大意
给定一棵$n$个节点的树,维护$n$个集合,一开始第$i$个集合只有节点$i$。有$m$个操作,每次操作输入一个$(u,v)$,表示将$(u,v)$这条链上所有点所属的集合合并。求有多少个无序数对$(u,v)$使得$u,v$同在一个集合之中。
$$n,mleq 10^5$$
思路
其实这道题要维护的就是极大联通子集使得这个集合的节点都讲同一种语言(以下简称联通块)的合并和维护大小。
设$s_x$表示$x$所在的联通块的大小,$ans=frac{1}{2}sum_{x}s_x$.
首先我们发现一个操作$(u,v)$即使将沿途上的联通块都合并在一起,这个问题目前还非常麻烦,但我们可以考虑一个稍微简单的问题,如何将$u$这个点加到$S$集合中。
这其实非常类似于建虚树,将$u$加入$S$其实就是找一个$S$中距离$u$最近的一个节点$v$,然后将$(u,v)$这条链上的点计入联通块,这个集合就会多$dep_u-dep_{lca(u,v)}$。
如果我们按照dfs序的顺序来更新的话,那么$v$这个点其实就是上一个加入的节点。
那对于第一个,即dfs序最小的那个点应该怎么办?先假设1号点在这个集合,但其实$(1,dep_{lca})$这条链上是不算的($lca$表示所有节点的LCA,但其实就是dfs序最大的节点和最小的节点的LCA),所以查询的时候还要减去。
我们对每个节点都维护这么一个集合,考虑通过值域线段树(下标为dfs序)来维护,$[l,r]$维护这段区间中dfs序最大/最小的节点,和所有节点$dep$之和减去所有相邻节点lca的dep之和,查询的时候就是这个值减去dfs序最大、最小的两个点的lca的深度。
合并$(u,v)$沿途的联通块就是将这条链上所有节点的线段树中插入$u,v$两个节点。
对一条链上的线段树同时操作可以使用树上差分,合并到父亲的时候可以使用线段树合并。
使用RMQ求lca,总时间复杂度$O(nlog n)$,空间复杂度$O(nlog n)$。
实现
注意在线段树合并的时候,对于这种树上的问题,可以将子节点的线段树合并进父节点的线段树而不用多开一个新的线段树,这样可以节约空间,但对于其他情况就需要考虑一下了。
Code
1 #include<bits/stdc++.h> 2 #define Rint register int 3 using namespace std; 4 typedef long long LL; 5 const int N = 200003, M = N << 6; 6 vector<int> Edge[N], Del[N]; 7 int n, m, fa[N], st[18][N], dfn[N], tim, tot, pre[N], dep[N]; 8 inline void dfs1(int x){ 9 dfn[x] = ++ tim; 10 pre[x] = ++ tot; st[0][tot] = x; 11 for(Rint v : Edge[x]) if(v != fa[x]) { 12 fa[v] = x; 13 dep[v] = dep[x] + 1; 14 dfs1(v); 15 st[0][++ tot] = x; 16 } 17 } 18 int lg2[N]; 19 inline void init(int k){ 20 lg2[1] = 0; 21 for(Rint i = 2;i <= k;i ++) lg2[i] = lg2[i >> 1] + 1; 22 for(Rint i = 1;i <= 17;i ++) 23 for(Rint j = 1;j <= k - (1 << i - 1);j ++) 24 st[i][j] = dep[st[i - 1][j]] < dep[st[i - 1][j + (1 << i - 1)]] ? st[i - 1][j] : st[i - 1][j + (1 << i - 1)]; 25 } 26 inline int LCA(int u, int v){ 27 u = pre[u]; v = pre[v]; 28 if(u > v) swap(u, v); 29 int k = lg2[v - u + 1]; 30 return dep[st[k][u]] < dep[st[k][v - (1 << k) + 1]] ? st[k][u] : st[k][v - (1 << k) + 1]; 31 } 32 int rt[N], seg[M], c[M], s[M], t[M], ls[M], rs[M], num; 33 /* 34 rt : root nodes 35 seg : sum_{xin T} dep[x] 36 c : sum_{xin T} 1 37 s : min_{xin T} x 38 t : max_{xin T} x 39 ls, rs : left/right node 40 */ 41 inline void pushup(int x){ 42 seg[x] = seg[ls[x]] + seg[rs[x]] - dep[LCA(t[ls[x]], s[rs[x]])]; 43 s[x] = s[ls[x]] ? s[ls[x]] : s[rs[x]]; 44 t[x] = t[rs[x]] ? t[rs[x]] : t[ls[x]]; 45 } 46 inline int query(int x){return seg[x] - dep[LCA(s[x], t[x])];} 47 inline void change(int &x, int p, int v, int L, int R){ 48 if(!x) x = ++ num; 49 if(L == R){ 50 c[x] += v; 51 seg[x] = c[x] ? dep[p] : 0; 52 s[x] = t[x] = c[x] ? p : 0; 53 return; 54 } 55 int mid = L + R >> 1; 56 if(dfn[p] <= mid) change(ls[x], p, v, L, mid); 57 else change(rs[x], p, v, mid + 1, R); 58 pushup(x); 59 } 60 inline void merge(int &x, int v, int L, int R){ 61 if(!x || !v){x |= v; return;} 62 if(L == R){ 63 c[x] += c[v]; seg[x] |= seg[v]; s[x] |= s[v]; t[x] |= t[v]; 64 return; 65 } 66 int mid = L + R >> 1; 67 merge(ls[x], ls[v], L, mid); 68 merge(rs[x], rs[v], mid + 1, R); 69 pushup(x); 70 } 71 LL ans; 72 inline void dfs2(int x){ 73 for(Rint v : Edge[x]) 74 if(v != fa[x]) dfs2(v); 75 for(Rint v : Del[x]) 76 change(rt[x], v, -1, 1, n); 77 ans += query(rt[x]); 78 if(fa[x]) merge(rt[fa[x]], rt[x], 1, n); 79 } 80 int main(){ 81 scanf("%d%d", &n, &m); 82 for(Rint i = 1;i < n;i ++){ 83 int a, b; 84 scanf("%d%d", &a, &b); 85 Edge[a].push_back(b); 86 Edge[b].push_back(a); 87 } 88 dfs1(1); init(tot); 89 for(Rint i = 1;i <= m;i ++){ 90 int u, v, lca; 91 scanf("%d%d", &u, &v); 92 lca = LCA(u, v); 93 // printf("u = %d, v = %d, lca = %d ", u, v, lca); 94 change(rt[u], v, 1, 1, n); change(rt[v], u, 1, 1, n); 95 change(rt[v], v, 1, 1, n); change(rt[u], u, 1, 1, n); 96 Del[lca].push_back(u); Del[lca].push_back(v); 97 if(fa[lca]) Del[fa[lca]].push_back(u), Del[fa[lca]].push_back(v); 98 } 99 // for(Rint i = 1;i <= n;i ++) 100 // printf("dep[%d] = %d ", i, dep[i]); 101 dfs2(1); 102 printf("%lld ", ans >> 1); 103 }
闲聊
如果大家想要进一步深入了解线段树合并如何做树上联通块问题,或者是其他树上联通块问题有什么做法,可以看看rxd的《解决树上联通块问题的一些技巧和工具》。