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

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

  • 算法内容
    • 前置知识:启发式合并(dsu)
      • 例题:[HNOI2009] 梦幻布丁
    • 重点:树上启发式合并(dsu on tree)
  • 例题
    • #1 Tree Requests
    • #2 Blood Cousins Return
    • #3 Arpa’s letter-marked tree and Mehrdad’s Dokhtar-kosh paths
    • #4 [NOIP2016 提高组] 天天爱跑步
    • #5 树上统计
    • 其余练习题

算法内容

前置知识:启发式合并(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 //考虑类似点分治的思想,每次只统计经过当前点的路径
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 //起点好统计,终点不好统计
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

你可能感兴趣的:(个人总结,内容总结,算法,数据结构,c++)