启发式合并(dsu),树上启发式合并(dsu on tree)总结

算法内容

前置知识:启发式合并(dsu)


\qquad 想学习“树上”启发式合并,就要先知道什么是“启发式合并”。

启发式算法是基于人类的经验和直观感觉,对一些算法的优化。 —— oi-wiki

\qquad 启发式合并的典型例子是并查集的启发式合并,即按秩合并。在合并两个并查集的时候,我们可以选择让深度大小较小的一个并查集并到另一个上面。这种合并方式虽然看起来很暴力,但是时间复杂度可以证明是 Θ ( n log ⁡ n ) \Theta(n\log n) Θ(nlogn) 的。还有像 set \text{set} set, vector \text{vector} vector 这类的数据结构也是可以直接启发式合并的,时间复杂度证明类似于并查集的按秩合并。

\qquad 启发式合并是一种优雅的暴力,看起来跟暴力差不多,但是实际上时间复杂度是严格正确的。

例题:[HNOI2009] 梦幻布丁


\qquad 题面

\qquad 本题就是一个典型的 set \text{set} set 的启发式合并,直接暴力做即可。实现时有一点点小细节,还有一个至关重要的小 trick \text{trick} trick S T L STL STL 中的 set , vector \text{set}, \text{vector} set,vector 是可以直接使用 swap \text{swap} swap 函数的,时间复杂度 Θ ( 1 ) \Theta(1) Θ(1)

\qquad 核心 Code : \text{Code}: Code:

//opt=1 if(x == y) continue; int fx = x, fy = y; if(col[fx].size() < col[fy].size()) { for(it1 = col[fx].begin(); it1 != col[fx].end(); it1 ++) { int v = *it1; if(it1 != col[fx].begin()) it2 = it1, it2 --;//如果it1不是开头迭代器,就找到上一个迭代器,存在it2中 if((it1 == col[fx].begin() || (it1 != col[fx].begin() && *it2 + 1 != *it1)) && col[fx].find(v - 1) == col[fx].end() && col[fy].find(v - 1) != col[fy].end()) per --;//当it1与前一个迭代器所指的值不相邻的时候再判断,因为如果相邻,那么他们本来就是一段,修改完还是一段,没有判断的必要,处理不好还可能误判 if(it1 != col[fx].end()) it2 = it1, it2 ++; if((it1 == -- col[fx].end() || (it1 != col[fx].end() && *it1 + 1 != *it2)) && col[fx].find(v + 1) == col[fx].end() && col[fy].find(v + 1) != col[fy].end()) per --; c[v] = fy, col[fy].insert(v); } col[fx].clear(); } else { for(it1 = col[fy].begin(); it1 != col[fy].end(); it1 ++) { int v = *it1; if(it1 != col[fy].begin()) it2 = it1, it2 --; if((it1 == col[fy].begin() || (it1 != col[fy].begin() && *it2 + 1 != *it1)) && col[fy].find(v - 1) == col[fy].end() && col[fx].find(v - 1) != col[fx].end()) per --; if(it1 != col[fy].end()) it2 = it1, it2 ++; if((it1 == -- col[fy].end() || (it1 != col[fy].end() && *it1 + 1 != *it2)) && col[fy].find(v + 1) == col[fy].end() && col[fx].find(v + 1) != col[fx].end()) per --; c[v] = fx, col[fx].insert(v); } col[fy].clear(); swap(col[fx], col[fy]);

重点:树上启发式合并(dsu on tree)


\qquad 知道了什么是启发式合并后,进一步就该学习树上启发式合并了。

\qquad 树上启发式合并主要是用来解决一些允许离线的子树统计问题、点对统计问题,可以套树形 dp \text{dp} dp,某些路径统计问题也可以解决。树上莫队、线段树合并这类算法能解决的问题有许多树上启发式合并也能解决。而且对于某些询问不统一的问题(即每个节点询问的内容相同,但某些要求不同)也可以解决。

\qquad 其实树上启发式合并最主要是利用了启发式合并的思想。启发式合并的时候,我们可以通过让小的合并到大的上来保证时间复杂度。那么在树上,我们怎样才能保证时间复杂度正确呢?

\qquad 我们以前学过一种名为树链剖分的算法,在这一算法中,我们引入了轻、重儿子的概念,并分析了其性质以及其为什么能保证复杂度。在 dsu on tree \text{dsu on tree} dsu on tree 中,我们也可以借用轻重儿子来保证时间复杂度。具体的,树上启发式合并分为以下几步:

  1. 递归轻儿子,解决轻儿子中的询问,并清空轻儿子中的信息;
  2. 递归重儿子,解决本条重链中其他点的询问,不清空重链的信息
  3. 将轻儿子和自己的信息加入进来,然后处理自己的询问。如果自己是个轻儿子,那么清空所有信息;否则不清空。

\qquad 这么做看起来,十分甚至九分的暴力。那么就又到了经典环节:我们来分析一下时间复杂度。这一做法看起来暴力,主要在于一个点被访问的次数看起来很多。我们想:一个点被访问到只有两种情况:1、计算当前点答案;2、作为轻子树中的点,加入信息。不难发现,情况 1 1 1 只会进行一次,而根据轻边的性质,可以轻易得知情况二最多只会被统计到 Θ ( log ⁡ n ) \Theta(\log n) Θ(logn) 次。所以整体复杂度为 Θ ( n log ⁡ n ) \Theta(n\log n) Θ(nlogn)

\qquad 树上启发式合并的拓展性极强。而且因为本身时间复杂度为 Θ ( n log ⁡ n ) \Theta(n\log n) Θ(nlogn),所以有时还可以套一些复杂度 Θ ( log ⁡ n ) \Theta(\log n) Θ(logn) 的数据结构。

\qquad 对于这类写起来较为繁琐的算法,定一个该算法的板子、整体框架显然是个极好的决定:

/* define: dfn[x]:x的dfn序(大多数用不到) L[x]/R[x]:以x为根的子树的dfn序范围 sze[x]:以x为根的子树大小 son[x]:x的重儿子 */ //----------------------------- void dfs_pre(int x) {//预处理出有用信息 sze[x] = 1, dfn[x] = ++ num, L[x] = num, V[num] = x; for(int i = head[x]; i; i = edge[i].lst) { int v = edge[i].to; dfs_pre(v); sze[x] += sze[v]; if(sze[v] > sze[son[x]]) son[x] = v; } R[x] = num; } inline void ins(int x, int y) {//单点贡献 } void update(int x, int y) {//子树贡献 for(int i = L[x]; i <= R[x]; i ++) ins(V[i], y); } void query(int x) {//计算对x的询问的答案 } void dfs(int x, bool flag) {//flag:判断是不是重儿子 for(int i = head[x]; i; i = edge[i].lst) {//递归轻儿子 int v = edge[i].to; if(v != son[x]) dfs(v, 0); } if(son[x]) dfs(son[x], 1);//递归重儿子 for(int i = head[x]; i; i = edge[i].lst) { int v = edge[i].to; if(v != son[x]) update(v, 1);//加入轻儿子信息 } ins(x, 1);//加入自身信息 query(x);//处理询问 if(!flag) update(x, -1);//轻儿子的话清空 } int main() { //init dfs_pre(1), dfs(1, 0); //print return 0; }

例题


#1 Tree Requests


\qquad 题面

\qquad 本题是个很典型的子树统计(虽然有深度限制),而且询问不统一,显然可以用 dsu on tree \text{dsu on tree} dsu on tree 解决。一堆字符能构成回文串有什么条件呢?显然是:出现次数为奇数的字符个数小于等于 1 1 1。那么我们记录一下某一深度,某一字符的出现次数,然后询问的时候枚举 26 26 26 个字符,记录有几个出现次数为奇数即可顺利解决。

\qquad 既然 dsu on tree \text{dsu on tree} dsu on tree 已经定板了,那么我们只需小小修改一下 ins,query \text{ins},\text{query} insquery 函数即可。

\qquad 核心 Code \text{Code} Code

inline void ins(int x, int y) { cnt[dep[x]][s[x] - 'a' + 1] += y; } bool query(int d) { int res = 0; for(int i = 1; i <= 26; i ++) res += (cnt[d][i] & 1); return res <= 1;//回文串条件:出现奇数次的字符 <= 1个 }

#2 Blood Cousins Return


\qquad 题面

\qquad 本题也是典型的询问不统一。但是因为本题查询的是有几个不同的字符串,显然要用一个时间复杂度为 Θ ( log ⁡ n ) \Theta(\log n) Θ(logn) 的数据结构: map \text{map} map 来存。

\qquad 核心 Code \text{Code} Code

void ins(int x, int y) {//cnt:某一深度出现了几个不同的字符串 if(y == 1) { flag[dep[x]][id[x]] ++; if(flag[dep[x]][id[x]] == 1) cnt[dep[x]] ++; } else { if(flag[dep[x]][id[x]] == 1) cnt[dep[x]] --; flag[dep[x]][id[x]] --; } }

#3 Arpa’s letter-marked tree and Mehrdad’s Dokhtar-kosh paths


\qquad 题面

\qquad 同样是回文串,但本题明显偏难,因为它查询的是最长回文串长度。如果查全局最长回文串长度,我们显然可以用点分治写。但是这道题是每一个子树都查。怎么办呢?我们仍然可以借用点分治的思想:在计算时,只计算经过当前子树根节点的路径的最长回文串长度,然后和子树内的取个 max ⁡ \max max 即可。

\qquad 既然“根”已经定了,那么两点间距离显然可以用 dep x + dep y − 2 × dep r o o t \text{dep}_x + \text{dep}_y - 2\times \text{dep}_{root} depx+depy2×deproot 表示。那么如何快速查询与自己相配对的回文串的最长长度呢?这就又用到了一步转化:因为一个回文串只允许最多有一个出现次数为奇数的字符,那么若一个字符出现了偶数次,那么我们可以看做它没出现过;同理,若一个字符出现了奇数次,那么我们可以看做它只出现了一次。再次观察题面:字符总数只有 22 22 22 个,那不是显然告诉我们要压成一个二进制数吗?但是,我们把哪一部分压成二进制数呢?压成二进制数之后该存在哪呢?

\qquad 既然想到了二进制数,我们就先不急着去处理上面两个问题,先想想什么状态是合法的。显然当这个二进制数只有一位为 1 1 1,或它本身就是 0 0 0 时是合法的。而我们根据点分治的思想,显然需要让两个不同子树中的点到子树根的路径相拼接,得到一条合法路径。到此,上面两个问题就迎刃而解:我们要把从子树根到子树内每一个点压成一个二进制数,存在这条路径的终点处。那么这两条被拼接的路径需要满足什么要求呢?显然的是,我们如果把它们分别看作两个二进制数,那么这两个二进制数异或起来一定是个合法状态。想到了这一步,我们接着想异或操作有什么性质?两个相同的数异或起来为 0 0 0!虽然会异或的人都知道这一基本性质,但是你不得不承认它很有用。有用在哪呢?我们想:若对于每一个子树根,每次都枚举一遍子树、把每一条路径压成二进制数并储存,时间复杂度显然直接变大变高。但是根据异或的重要性质,我们显然只用存 1 1 1 号点(树根)到每个点的路径,每次一异或就把子树根以上的路径给异或掉了,就能直接计算了。

\qquad 本题还有两个至关重要的小细节:1、既然是点分治的思想,那么就要先计算再添加;2、桶数组初值要赋为极小值。

\qquad 核心 Code \text{Code} Code

#include <bits/stdc++.h>//考虑类似点分治的思想,每次只统计经过当前点的路径 using namespace std;//错因:在同一子树内时,边加边计算 为了计算只经过当前点的路径,应该不同子树内计算 const int maxn = 5e5 + 10; int n; struct pic { int to, lst; }edge[maxn]; int head[maxn], tot = 0, dfn[maxn], num = 0, L[maxn], R[maxn], V[maxn], son[maxn], sze[maxn], dep[maxn]; int val[maxn], ans[maxn], cnt[(1 << 22) + 5];//val[i]:从根到i的路径上,每种字符出现的奇偶性 cnt[i]:达到状态为i的最大深度 inline void add(int x, int y) { edge[++ tot] = {y, head[x]}; head[x] = tot; } void dfs_pre(int x) { sze[x] = 1, dfn[x] = ++ num, L[x] = num, V[num] = x; for(int i = head[x]; i; i = edge[i].lst) { int v = edge[i].to; //把从根到每个点的路径压成二进制数 val[v] ^= val[x], dep[v] = dep[x] + 1; dfs_pre(v); sze[x] += sze[v]; if(sze[v] > sze[son[x]]) son[x] = v; } R[x] = num; } inline void ins(int x, int y) {//单点修改 cnt[val[x]] = (y == -1 ? 0xcfcfcfcf : max(cnt[val[x]], dep[x])); } void update(int x, int y) { for(int i = L[x]; i <= R[x]; i ++) ins(V[i], y); } void Get(int root, int x) {//单点查询 当前枚举到点x,子树根为root for(int i = 0; i < 22; i ++) ans[root] = max(ans[root], dep[x] + cnt[val[x] ^ (1 << i)] - (dep[root] << 1)); ans[root] = max(ans[root], dep[x] + cnt[val[x]] - (dep[root] << 1)); } void solve(int root, int x) { for(int i = L[x]; i <= R[x]; i ++) Get(root, V[i]); } void dfs(int x, bool flag) { for(int i = head[x]; i; i = edge[i].lst) { int v = edge[i].to; if(v != son[x]) dfs(v, 0); } if(son[x]) dfs(son[x], 1); for(int i = head[x]; i; i = edge[i].lst) { int v = edge[i].to; if(v != son[x]) solve(x, v), update(v, 1); } Get(x, x); ins(x, 1); for(int i = head[x]; i; i = edge[i].lst) ans[x] = max(ans[x], ans[edge[i].to]);//记得跟儿子们的答案取个max,因为儿子们也算x子树内的 if(!flag) update(x, -1); } int main() { scanf("%d", &n); memset(cnt, 0xcf, sizeof cnt);//初值极小值!!!!!!!!!!!!!!!!!!!!!!!!! for(int i = 2, f; i <= n; i ++) { char ch; scanf("%d\n%c", &f, &ch); val[i] = (1 << (ch - 'a')), add(f, i); } dep[1] = 1, dfs_pre(1); dfs(1, 0); for(int i = 1; i <= n; i ++) printf("%d ", ans[i]); puts(""); return 0; }

#4 [NOIP2016 提高组] 天天爱跑步


\qquad 题面

\qquad 本题做法很多, dsu on tree \text{dsu on tree} dsu on tree 显然也是不二之选。但是本题乍一看,和 dsu on tree \text{dsu on tree} dsu on tree 一点点关系也没有啊……那我们显然需要将问题认真分析一下。

\qquad 首先,我们考虑对于一个点 x \text{x} x,那些路径可能被 x \text{x} x 统计到?最基本的条件显然是这条路径有一个端点在以 x \text{x} x 为根的子树中。这个条件满足了,接下来就该是跑步时间的限制了。那么我们想,被统计到的路径是不是可以分为两类:1、起点在以 x \text{x} x 为根的子树中(不管终点位置);2、终点在以 x \text{x} x 为根的子树中(不管起点位置)。对于这两种路径,我们分别处理。

\qquad 对于起点在子树内的路径,实际上是很好统计的。因为此时 w x \text{w}_x wx 就相当于 x \text{x} x 与起点的深度差。但是对于终点,怎么统计呢?

\qquad 假设 t \text{t} t 是一个合法的终点, T \text{T} T 是这个终点所在路径的长度,那么这个 t \text{t} t 要满足什么条件呢?如果列出式子,那应该是这样的: w x + ( dep t − dep x ) = T \large \text{w}_x+(\text{dep}_t-\text{dep}_x)=\text{T} wx+(deptdepx)=T,简单移项后发现 w x − dep x = T − dep t \large \text{w}_x-\text{dep}_x=\text{T}-\text{dep}_t wxdepx=Tdept。此时,我们发现等号左边只与 x \text{x} x 有关,等号右边只与 t \text{t} t 有关,那么我们显然可以看成点对统计问题直接开桶解决。

\qquad 起点和终点都解决了,那这题解决了吗?显然没有:有可能这条路径的 lca \text{lca} lca 刚好是 x \text{x} x,那么这条路径便会在起点和终点分别被统计一次。这时我们便需要考虑去重。去重也很好写:我们只需将一条路径在 lca \text{lca} lca 处存一下,在计算完后遍历以 x \text{x} x lca \text{lca} lca 的路径判断一下即可。

\qquad 还有一个至关重要的细节:一条路径肯定不会被 lca \text{lca} lca 之上的节点统计到,所以我们在遍历以 x \text{x} x lca \text{lca} lca 的路径时,要顺便把这些路径在桶中的贡献清空。这样,本题就完美解决了。

\qquad Code \text{Code} Code

#include <bits/stdc++.h>//起点好统计,终点不好统计 using namespace std;//统计终点,相当于找到一个j,使得t[j]-w[i]=dep[j]-dep[i],移项可得t[j]-dep[j]=w[i]-dep[i],用一个数组存t[j]-dep[j]即可统计 const int maxn = 3e5 + 10; const int base = 3e5 + 1;//t[j]-dep[j]可能为负数,所以需整体加个偏移量 int n, m; struct pic { int to, lst; }edge[maxn << 1]; int head[maxn], tot = 0, dfn[maxn], num = 0, L[maxn], R[maxn], V[maxn], dep[maxn], f[maxn][25], son[maxn], sze[maxn]; int w[maxn], cnt0[maxn << 1], cnt1[maxn << 1], s[maxn], t[maxn], ans[maxn], len[maxn], l[maxn]; //cnt0[i]:深度为i处有几个起点 S/T[i]:点i为几条路径的起点/终点 cnt1[i]:存t[j]-dep[j]=i的j有几个,查询的时候查cnt[w[i]-dep[i]] vector < int > mark[maxn];//存以i为lca的路径 vector < int > ed[maxn], st[maxn];//存以i为起点/终点的路径 inline void add(int x, int y) { edge[++ tot] = {y, head[x]}; head[x] = tot; } void dfs_pre(int x, int fa) { sze[x] = 1, dep[x] = dep[fa] + 1, f[x][0] = fa, dfn[x] = ++ num, L[x] = num, V[num] = x; for(int i = 1; i < 25; i ++) f[x][i] = f[f[x][i - 1]][i - 1]; for(int i = head[x]; i; i = edge[i].lst) { int v = edge[i].to; if(v == fa) continue; dfs_pre(v, x); sze[x] += sze[v]; if(sze[v] > sze[son[x]]) son[x] = v; } R[x] = num; } int lca(int x, int y) { if(dep[x] < dep[y]) swap(x, y); for(int i = 24; i >= 0; i --) if(dep[f[x][i]] >= dep[y]) x = f[x][i]; if(x == y) return x; for(int i = 24; i >= 0; i --) if(f[x][i] != f[y][i]) x = f[x][i], y = f[y][i]; return f[x][0]; } void ins(int root, int x, int y) {//起点/终点能产生贡献,当且仅当本条路径穿过root for(auto p : st[x]) { if(dep[l[p]] <= dep[root]) cnt0[dep[x]] += y; } for(auto p : ed[x]) { if(dep[l[p]] <= dep[root]) cnt1[len[p] - dep[x] + base] += y; } } void update(int root, int x, int y) { for(int i = L[x]; i <= R[x]; i ++) ins(root, V[i], y); } void query(int x) { ans[x] += cnt0[dep[x] + w[x]] + cnt1[w[x] - dep[x] + base]; } void del(int x, bool flag) { for(auto p : mark[x]) { if(dep[s[p]] == dep[x] + w[x]) ans[x] --;//把重复计算的删掉 if(flag) cnt0[dep[s[p]]] --, cnt1[len[p] - dep[t[p]] + base] --;//只有当x是重儿子时才删!!!!!因为若x是轻儿子在下面就会被清空了!!!!! } } void dfs(int x, int fa, bool flag) { for(int i = head[x]; i; i = edge[i].lst) { int v = edge[i].to; if(v == fa || v == son[x]) continue; dfs(v, x, 0); } if(son[x]) dfs(son[x], x, 1); for(int i = head[x]; i; i = edge[i].lst) { int v = edge[i].to; if(v == fa || v == son[x]) continue; update(x, v, 1); } ins(x, x, 1); query(x); del(x, flag);//把lca为x的路径产生的贡献删除,因为上面的点一定统计不到这些路径 if(!flag) update(x, x, -1); } int main() { scanf("%d%d", &n, &m); for(int i = 1, x, y; i < n; i ++) scanf("%d%d", &x, &y), add(x, y), add(y, x); for(int i = 1; i <= n; i ++) scanf("%d", &w[i]); dfs_pre(1, 0); for(int i = 1; i <= m; i ++) { scanf("%d%d", &s[i], &t[i]); l[i] = lca(s[i], t[i]); len[i] = dep[s[i]] + dep[t[i]] - (dep[l[i]] << 1); mark[l[i]].push_back(i), ed[t[i]].push_back(i), st[s[i]].push_back(i);//在lca,s,t处分别存一下本条路径 } dfs(1, 0, 0); for(int i = 1; i < n; i ++) printf("%d ", ans[i]); printf("%d\n", ans[n]); return 0; }

#5 树上统计


\qquad 题面

\qquad 对于本题,第一思路肯定是求解有多少区间跨越了这条边。但是这显然很难求,于是我们考虑正难则反,统计被一条边分开的两个连通块中分别包含了多少个连续区间,然后用总贡献减去这些不经过本条边的区间。那我们的问题就转化为:被一条边分开的两个连通块中分别包含了多少连续区间。

\qquad 对于动态维护连续区间这类问题,我们显然可以通过维护每一段左右端点来轻松做到。具体的,我们开一个桶记录每个位置是否在子树内出现过,然后我们分别统计有多少个连续 1 1 1,有多少个连续 0 0 0 即可。因为随着运算的进行,这个桶维护的信息只增不删,那么维护连续 1 1 1 显然是很好做的:维护每一段连续 1 1 1 的左右端点即可。但是对于连续 0 0 0 怎么搞呢?我们可以考虑开一个 set \text{set} set 来存当前存在的 0 0 0 区间,每次把一个 0 0 0 改为 1 1 1 时,找到包含当前 0 0 0 的区间并进行相应操作即可。

\qquad 核心 Code \text{Code} Code

/* define #define MP make_pair #define PII pair < int , int > #define F first #define S second all:最终答案,all=开始的总贡献-不会产生的贡献 开始的总贡献=(n*(n+1)/2)*(n-1),即:空的序列共有n*(n+1)/2个区间,共有(n-1)条边 zero:连续0区间(子树外的连续区间) one:连续1区间(子树内的连续区间) */ // inline LL Get(int x) {//一段长为x的连续段能构成的区间总数 return 1LL * x * (x + 1) / 2LL; } void ins(int x, int y) { if(y == 1) {//add 0 -> 1 PII lst = *s.lower_bound(MP(-x, -1e9)); s.erase(lst); int len = (-lst.S) - (-lst.F) + 1; zero -= Get(len); if((-lst.F) != x) {//左端点不相同 len = (x - 1) - (-lst.F) + 1; zero += Get(len); s.insert(MP(lst.F, -(x - 1))); } if((-lst.S) != x) {//右端点不相同 len = (-lst.S) - (x + 1) + 1; zero += Get(len); s.insert(MP(-(x + 1), lst.S)); } if(cnt[x - 1] && cnt[x + 1]) {//合并两段1 int lenl = ro[x - 1] - lo[x - 1] + 1, lenr = ro[x + 1] - lo[x + 1] + 1; one -= Get(lenl) + Get(lenr); int len_all = ro[x + 1] - lo[x - 1] + 1; one += Get(len_all); lo[ro[x + 1]] = lo[x - 1], ro[lo[x - 1]] = ro[x + 1]; } else if(cnt[x - 1] && !cnt[x + 1]) {//扩展一段1 int lenl = ro[x - 1] - lo[x - 1] + 1; one -= Get(lenl); one += Get(lenl + 1); ro[lo[x - 1]] = x, lo[x] = lo[x - 1], ro[x] = x; } else if(!cnt[x - 1] && cnt[x + 1]) { int lenr = ro[x + 1] - lo[x + 1] + 1; one -= Get(lenr); one += Get(lenr + 1); lo[ro[x + 1]] = x, lo[x] = x, ro[x] = ro[x + 1]; } else {//单独变成1 one += Get(1); lo[x] = ro[x] = x; } cnt[x] = 1; } else {//del 1 -> 0 lo[x] = ro[x] = 0; cnt[x] = 0; } } void update(int x, int y) { for(int i = L[x]; i <= R[x]; i ++) ins(V[i], y); if(y == -1) {//清空 set < PII > ss; swap(ss, s);//小trick s.insert(MP(-1, -n)); one = 0, zero = Get(n); } } void dfs(int x, int fa, bool flag) { for(int i = head[x]; i; i = edge[i].lst) { int v = edge[i].to; if(v == fa || v == son[x]) continue; dfs(v, x, 0); } if(son[x]) dfs(son[x], x, 1); for(int i = head[x]; i; i = edge[i].lst) { int v = edge[i].to; if(v == fa || v == son[x]) continue; update(v, 1); } ins(x, 1); if(x != 1) all -= zero + one;//统计答案,相当于一个点对应它的父边,1号点没有父边 if(!flag) update(x, -1); }

其余练习题

\qquad Tree and Queries

\qquad 树上数颜色

\qquad Dominant Indices


__EOF__

本文作者best_brain
本文链接https://www.cnblogs.com/best-brain/p/18006554.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是博主的最大动力!
posted @   best_brain  阅读(581)  评论(0编辑  收藏  举报  
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· .NET10 - 预览版1新功能体验(一)
点击右上角即可分享
微信分享提示