dsu on tree 简单总结
什么是dsu on tree:
对于一棵树,如果我们需要统计每一个点的子树的信息,比如求子树中哪个点权出现的次数最多(最大值这种就是树剖裸题了搞什么$dsu$ $on$ $tree$?)这样子,如果我们对于每一个子树都进行遍历的话,时间复杂度将达到$O(n^2)$,当$n \leq 1e5$的时候,这个时间复杂度无法接受。那么我们应该怎么办呢?这个时候就需要使用$dsu$ $on$ $tree$。
我们知道,树链剖分可以把一棵树分成$O(log_2 n)$条重链,剩下的都是轻边。现在证明一下从根到某结点经过的轻边的数量:因为每经过一条轻边,结点数是一半,所以$size \leq \frac{n}{2^{edge}}$,又$n > 2^{edge}$,所以$edge<log_2 n$。即只会经过$O(log_2 n)$条轻边。
我们先从上到下遍历结点,然后对于某个节点,我们访问所有的轻儿子,然后求得了这些轻儿子的子树信息,接下来我们考虑重儿子。访问重儿子,然后保留重儿子的信息,对于轻儿子就暴力处理。我们分析它的时间复杂度:我们假设这棵子树的大小是$n$。则我们暴力统计所有轻儿子的时间复杂度是$O(log_2 n)$,保留重儿子信息,即重儿子信息$O(1)$上推,则我们就可以$O(log_2 n)$求出这棵子树的信息。
当然,树不能有修改,不然重儿子的信息就会失效。
并且,所有询问都针对子树。
其思想是启发式合并,因为我还没有学,所以先这样,学了再回来总结。
dsu on tree实现:
算法流程:
一、轻重链剖分
然后这个过程不好理解,我给出路径:
进入轻儿子直到叶节点,处理这个叶节点的信息,然后更新这个轻儿子答案,并删除轻儿子的信息,然后回溯到它的父亲,然后进入重儿子,假设这个重儿子是叶节点,我们处理重儿子的信息,保留信息并更新答案,然后回溯到它的父亲,它的父亲将会暴力统计这个轻儿子的信息,然后更新自己(因为重儿子信息没有删除),然后删除轻儿子的信息,这个节点处理完,回溯到上一层,这样子我们就保留了重儿子的信息以处理这个子树根结点的父亲。显然,因为本身我们进入的是一个轻儿子,所以在处理完它的父节点时,这个轻儿子的信息会被删除(这个建议画图理解,非常绕)。
例题:
1、codeforces600E Lomsat gelral
题意:
给出一棵树,求出每个节点的子树中出现次数最多的颜色的编号和。
题解:
直接$dsu$ $on$ $tree$,用桶记录颜色次数即可。
AC代码:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#include <bits/stdc++.h> using namespace std; typedef long long ll; const int MAXN = 1e5 + 5; vector<int> G[MAXN]; int col[MAXN], son[MAXN], sz[MAXN]; bool vis[MAXN]; ll tot[MAXN], ans[MAXN]; ll maxn, c; void dfs1(int u, int fa) { sz[u] = 1; for (auto i : G[u]) if (i != fa) { dfs1(i, u); sz[u] += sz[i]; if (sz[i] > sz[son[u]]) son[u] = i; } } void update(int u, int fa, int p) { tot[col[u]] += p; if (p > 0 && maxn == tot[col[u]]) c += col[u]; else if (p > 0 && maxn < tot[col[u]]) c = col[u], maxn = tot[col[u]]; for (auto i : G[u]) if (i != fa && !vis[i]) update(i, u, p); } void dfs2(int u, int fa, int op) { for (auto i : G[u]) if (i != son[u] && i != fa) dfs2(i, u, 0); if (son[u]) dfs2(son[u], u, 1), vis[son[u]] = 1; update(u, fa, 1), ans[u] = c; if (son[u]) vis[son[u]] = 0; if (!op) update(u, fa, -1), maxn = c = 0; } int main() { int n; scanf("%d", &n); for (int i = 1; i <= n; ++i) scanf("%d", &col[i]); int a, b; for (int i = 1; i < n; ++i) { scanf("%d%d", &a, &b); G[a].push_back(b); G[b].push_back(a); } dfs1(1, 0); //tot: n dfs2(1, 0, 0); //tot: n+2nlogn for (int i = 1; i <= n; ++i) printf("%lld%c", ans[i], i == n ? '\n' : ' '); return 0; }
注:为什么$update$之后$vis[son[u]]$要置零,这是因为,只要$op$是$0$,这个对于其父亲来说轻儿子,所以实际上执行到这里的时候,它的父亲的重儿子的信息处理好了,所以就要把这个轻儿子的所有信息全部删去,这个很绕。
2、codeforces570D Tree Requests
题意:
给出一棵树,树上每个节点上有一个小写字符,给出一些询问,询问包括节点和深度,求出以这个节点为根,固定深度的节点上能否组成一个回文串。
题解:
看满不满足使用条件:无修改,查询子树。满足条件,然后我们看维护什么,维护一个回文串,这个其实不好做,显然我们不能把这些深度的字符串都保存下来,因为这将达到$O(n^2)$的空间,所以我们只能用更加省空间的方案。一个字符串构成回文串,里面的同种字符的数量至多出现一个奇数,所以我们实际关注的是,这些字符串模$2$的和是不是小于$2$,因为模$2$就是二进制,所以我们直接用一个整数维护这个字符串,用异或增减字符即可。
为什么我们这里可以处理相对某个结点深度大于$1$的结点呢?这是因为处理完重儿子之后到删除轻儿子信息之前,这棵子树的任何一个结点除了它自己的信息都是知道的,因为重儿子信息被保存,轻儿子子树的所有结点信息被处理出来,所以就可以把这个子树为根的询问全部回答了。
AC代码:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#include <bits/stdc++.h> using namespace std; const int MAXN = 5e5 + 5; vector<int> G[MAXN]; struct Q { int pos, dep; Q(int _pos, int _dep) : pos(_pos), dep(_dep) {} }; vector<Q> q[MAXN]; bool ans[MAXN]; int dep[MAXN], val[MAXN], sz[MAXN]; int tot[MAXN], son[MAXN]; bool vis[MAXN]; void dfs1(int u, int fa) { dep[u] = dep[fa] + 1; sz[u] = 1; for (auto i : G[u]) if (i != fa) { dfs1(i, u); sz[u] += sz[i]; if (sz[i] > sz[son[u]]) son[u] = i; } } bool get1(int val) { int rt = 0; while (val) rt += val % 2, val >>= 1; return rt < 2; } void update(int u, int fa) { tot[dep[u]] ^= val[u]; for (auto i : G[u]) if (i != fa && !vis[i]) update(i, u); } void del(int u, int fa) { tot[dep[u]] = 0; for (auto i : G[u]) if (i != fa && !vis[i]) del(i, u); } void dfs2(int u, int fa, int op) { for (auto i : G[u]) if (i != fa && i != son[u]) dfs2(i, u, 0); if (son[u]) dfs2(son[u], u, 1), vis[son[u]] = 1; update(u, fa); for (int i = 0; i < q[u].size(); ++i) ans[q[u][i].pos] = get1(tot[q[u][i].dep]); if (son[u]) vis[son[u]] = 0; if (!op) del(u, fa); } char str[MAXN]; int main() { int n, m; scanf("%d%d", &n, &m); int a; for (int i = 1; i < n; ++i) { scanf("%d", &a); G[i + 1].push_back(a); G[a].push_back(i + 1); } scanf("%s", str + 1); for (int i = 1; i <= n; ++i) val[i] = 1 << (str[i] - 'a'); int x, y; for (int i = 1; i <= m; ++i) { scanf("%d%d", &x, &y); q[x].push_back(Q(i, y)); } dfs1(1, 0); dfs2(1, 0, 0); for (int i = 1; i <= m; ++i) printf(ans[i] ? "Yes\n" : "No\n"); return 0; }
3、codeforces246E Blood Cousins Return
题意:
给出一棵族谱树,每个人都有一个名字,给出一些询问,求某个人的第$k$级儿子中有多少个不同的名字。
题解:
静态的子树信息询问,显然就是$dsu$ $on$ $tree$。用一个$set$维护每个深度的人名信息即可。注意:第$k$级儿子的深度可能超过树的最大深度,这个时候输出$0$,但是必须开够$set$否则会$RE$或者$WA$。
AC代码:(代码中的名字进行了离散化)
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#include <bits/stdc++.h> #pragma GCC optimize(2) using namespace std; typedef long long ll; const int N = 1e5 + 5; set<int> s[N << 1]; vector<int> G[N]; pair<int, int> p[N]; vector<pair<int, int>> q[N]; int ans[N]; string a[N], b[N]; int id[N]; int dep[N], son[N], sz[N]; void dfs1(int u, int fa) { sz[u] = 1; dep[u] = dep[fa] + 1; for (auto i : G[u]) if (i != fa) { dfs1(i, u); sz[u] += sz[i]; if (sz[i] > sz[son[u]]) son[u] = i; } } bool vis[N]; void add(int u, int fa, int op) { if (op == 1) s[dep[u]].insert(p[u].first); else s[dep[u]].clear(); for (auto i : G[u]) if (i != fa && !vis[i]) add(i, u, op); } void dfs2(int u, int fa, int op) { for (auto i : G[u]) if (i != fa && i != son[u]) dfs2(i, u, 0); if (son[u]) dfs2(son[u], u, 1), vis[son[u]] = 1; add(u, fa, 1); for (auto i : q[u]) ans[i.first] = s[i.second + dep[u]].size(); if (son[u]) vis[son[u]] = 0; if (!op) add(u, fa, -1); } int main() { ios::sync_with_stdio(0); cin.tie(0); int n; cin >> n; int top = 0; for (int i = 1; i <= n; ++i) { cin >> a[i] >> p[i].second; b[++top] = a[i]; } sort(b + 1, b + top + 1); top = unique(b + 1, b + top + 1) - (b + 1); for (int i = 1; i <= n; ++i) p[i].first = lower_bound(b + 1, b + top + 1, a[i]) - b; int rt = 0; for (int i = 1; i <= n; ++i) { G[p[i].second].push_back(i); G[i].push_back(p[i].second); } dfs1(rt, 0); int m; cin >> m; for (int i = 1; i <= m; ++i) { int x, y; cin >> x >> y; q[x].push_back({i, y}); } dfs2(rt, 0, 0); for (int i = 1; i <= m; ++i) cout << ans[i] << '\n'; return 0; }
4、codeforces208E Blood Cousins
题意:
给出一棵族谱树,给出一些询问,求和一个人有相同的第$k$级祖先的人有多少。
题解:
老$dsu$ $on$ $tree$了。但是这次是先求第$k$级祖先然后转化成上面第3题。其实这个$dsu$ $on$ $tree$在求解的时候,是在访问到求某个结点为根的子树的信息,这个第$k$级祖先就是根,而不能直接减一下深度然后放进存储询问的容器,这是初学者常犯的错误。所以我们就要写一个倍增预处理一下第$2^k$级祖先(当然你也可以长链剖分),这个时候再把询问存储好,然后就转化成了上题,只不过这里不用去重,所以不用$set$用$vector$。$vector$的$size$减一就是答案,注意,特判祖先不存在,此时输出$0$。
AC代码:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#include <bits/stdc++.h> using namespace std; const int N = 1e5 + 5; vector<int> G[N]; int son[N], sz[N], dep[N]; vector<int> s[N << 1]; vector<pair<int, int>> q[N]; int ans[N]; bool vis[N]; int f[N][20]; void init(int n) { for (int j = 1; j < 20; ++j) for (int i = 1; i <= n; ++i) f[i][j] = f[f[i][j - 1]][j - 1]; } int getanc(int u, int p) { for (int i = 19; ~i; --i) if ((1 << i) & p) u = f[u][i]; return u; } void dfs1(int u, int fa) { sz[u] = 1; if (u) dep[u] = dep[fa] + 1; for (auto i : G[u]) if (i != fa) { dfs1(i, u); sz[u] += sz[i]; if (sz[i] > sz[son[u]]) son[u] = i; } } void add(int u, int fa, int op) { if (op == 1) s[dep[u]].push_back(u); else s[dep[u]].clear(); for (auto i : G[u]) if (i != fa && !vis[i]) add(i, u, op); } void dfs2(int u, int fa, int op) { for (auto i : G[u]) if (i != fa && i != son[u]) dfs2(i, u, 0); if (son[u]) dfs2(son[u], u, 1), vis[son[u]] = 1; add(u, fa, 1); for (auto i : q[u]) ans[i.first] = s[dep[u] + i.second].size(); if (son[u]) vis[son[u]] = 0; if (!op) add(u, fa, 0); } int main() { int n, a; scanf("%d", &n); for (int i = 1; i <= n; ++i) { scanf("%d", &a); f[i][0] = a; G[a].push_back(i); G[i].push_back(a); } init(n); dfs1(0, 0); int m; scanf("%d", &m); for (int i = 1; i <= m; ++i) { int x, y; scanf("%d%d", &x, &y); if (dep[x] - y <= 0) ans[i] = 1; else q[getanc(x, y)].push_back({i, y}); } dfs2(0, 0, 0); for (int i = 1; i <= m; ++i) printf("%d%c", ans[i] - 1, " \n"[i == m]); return 0; }
*5、codeforces741D Arpa’s letter-marked tree and Mehrdad’s Dokhtar-kosh paths
题意:
给出一棵树,树上有一个字母作为边权,对于每一个结点,在以它为根的子树中,找到一条最长的路径,使得路径上的字母重排后可以组成一个回文串。字符集:$'a'-'v'$。
题解:
这个题是一道$2700$的题,刷新了我通过的题的难度等级,看到询问静态子树信息,继续是$dsu$ $on$ $tree$。但是这个子树信息就不好维护了,我们怎么维护这个路径呢?
首先我们先考虑怎么重排能变成回文串,显然至多只有一种字母出现奇数次。这样子,我们就可以$0$代表偶数次,$1$代表奇数次,又因为字符集大小只有$22$,所以表示这个字符串的状态只有$2^{22}$种,同时可以使用异或改变字符串状态。
然后无论如何这个字符串的两个端点一定存在一个$lca$。根据深度的性质,我们实际上是求出$max(dep[v]+dep[u]-2*dep[lca])$,因为我们显然不能一次求两个值(不然就是$O(n^2)$了),所以我们就要知道了一个的情况下求另一个。首先我们考虑一种简单的情况,就是这个字符串的其中一个端点就是两端点的$lca$,这样子我们就是直接往子树里面深搜更新最大值就完事了。
这样子我们可以大胆地思考一下一般情况。