学习笔记树上启发式合并

Posted 繁凡さん

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了学习笔记树上启发式合并相关的知识,希望对你有一定的参考价值。

整理的算法模板合集: ACM模板

点我看算法全家桶系列!!!

实际上是一个全新的精炼模板整合计划


树上启发式合并

树上启发式合并(dsu on tree) 用于求解某些树上离线问题,时间复杂度和代码实现难度都较低。用于解决 可以被划分为一个个子树进行单独求解,没有修改操作,各个子儿子对答案的贡献容易添加与删除的树上离线问题。

模板

CF600E Lomsat gelral

Weblink

https://www.luogu.com.cn/problem/CF600E

Problem

有一棵 n n n 个结点的以 1 1 1 号结点为根的有根树。每个结点都有一个颜色,颜色是以编号表示的, i i i 号结点的颜色编号为 c i c_i ci。如果一种颜色在以 x x x 为根的子树内出现次数最多,称其在以 x x x 为根的子树中占主导地位。显然,同一子树中可能有多种颜色占主导地位。你的任务是对于每一个 i ∈ [ 1 , n ] i\\in[1,n] i[1,n] ,求出以 i i i 为根的子树中,占主导地位的颜色的编号和。

n ≤ 1 0 5 , c i ≤ n n\\le 10^5,c_i\\le n n105,cin

Solution

显然问题可以简化为给出一棵 n n n 个节点以 1 1 1 为根的树,节点 i i i 的颜色为 c i c_i ci,现在对于每个结点 i i i 询问 i i i 子树里一共出现了多少种不同的颜色。

暴力对于每一个结点遍历子树统计颜色数复杂度显然是 O ( n 2 ) O(n^2) O(n2) 的。由于每个节点的答案由其子树和其本身得到考虑使用树上启发式合并来解决。

我们先对整棵树预处理出重儿子和轻儿子。对于每一个结点 x x x,遍历子树,维护 col_cnt 数组,我们先遍历 x x x 的所有的轻儿子,遍历所有轻儿子的子树,期间暴力修改 col_cnt 数组统计颜色(显然在最后回溯的时候再统计,即自底向上统计,返回对上层的贡献,这样就不会相互影响),并完成它们各自的子树内的所有询问,然后在退出轻儿子子树的时候暴力删掉该轻儿子子树对于 col_cnt 数组的所有影响。

然后再遍历重儿子,完成子树内的询问,退出重儿子子树的时候保留对 col_cnt 数组的影响,再次统计所有的轻儿子子树,将所有贡献加在一起,即可得到整颗子树的答案保留在 col_cnt 数组中,作为上层大树的一颗子树的贡献。

Code

#include <bits/stdc++.h> 
using namespace std;
using ll = long long;
const int N = 2e5 + 7, M = 3e6 + 7; 
int n, m, s, t;
ll ans[N];
ll col_cnt[N];//颜色出现次数
ll maxx, sum;
int head[N], ver[M], nex[M], edge[M], tot;
bool vis[N];
int siz[N], hson[N], col[N];

void add(int x, int y)
{
	ver[tot] = y;
	nex[tot] = head[x];
	head[x] = tot ++ ;
}

void dfs1(int x, int fa)
{
	siz[x] = 1;
	int h_son_size = -1, h_son = -1;
	for (int i = head[x]; ~i; i = nex[i]) {
		int y = ver[i];
		if(y == fa) continue;
		dfs1(y, x);
		siz[x] += siz[y];
		if(siz[y] > h_son_size) {
			h_son_size = siz[y];
			h_son = y;
		}
	}
	if(h_son != -1)
		hson[x] = h_son;
}

void dfs2(int x, int fa, int h_son)//暴力统计答案
{
	col_cnt[col[x]] ++ ;
	if(col_cnt[col[x]] > maxx) {
		maxx = col_cnt[col[x]];
		sum = col[x];
	}
	else if(col_cnt[col[x]] == maxx)
		sum += col[x];
	for (int i = head[x]; ~i; i = nex[i]) {
		int y = ver[i];
		if(y == fa || y == h_son) continue;
		dfs2(y, x, h_son);
	}
}

void init(int x, int fa)//暴力遍历子树删除答案
{
	col_cnt[col[x]] -- ;
	for (int i = head[x]; ~i; i = nex[i]) {
		int y = ver[i];
		if(y == fa) continue;
		init(y, x);
	}
}

void dsu_on_tree(int x, int fa)
{ 
	for (int i = head[x]; ~i; i = nex[i]) {
		int y = ver[i];
		if(y == fa) continue;
		if(hson[x] != y) {
			dsu_on_tree(y, x);
			init(y, x);
			sum = maxx = 0;
		} 
	}
	if(hson[x])
		dsu_on_tree(hson[x], x);
	dfs2(x, fa, hson[x]);
	ans[x] = sum;
}

int main()
{
	memset(head, -1, sizeof head);
	scanf("%d", &n);
	for (int i = 1; i <= n; ++ i)
		scanf("%d", &col[i]);
	for (int i = 1; i <= n - 1; ++ i) {
		int x, y;
		scanf("%d%d", &x, &y);
		add(x, y), add(y, x);
	}
	dfs1(1, 0);
	dsu_on_tree(1, 0);
	
	for (int i = 1; i <= n; ++ i)
		printf("%lld\\n", ans[i]); 
	return 0;
}

复杂度分析

时间复杂度严格 O ( n log ⁡ n ) O(n\\log n) O(nlogn)

根节点到树上任意节点的轻边数不超过 log ⁡ n \\log n logn 条。我们设根到该节点有 x x x 条轻边,该节点的子树大小为 y y y,显然轻边连接的子节点的子树大小小于父亲的一半(若大于一半就不是轻边了),则 y < n 2 x y<\\dfrac n {2^x} y<2xn,显然 n > 2 x n>2^x n>2x,所以 x < log ⁡ n x<\\log n x<logn。故每个轻儿子最多会被合并 log ⁡ n \\log n logn 次,总时间复杂度为 O ( n log ⁡ n ) O(n\\log n) O(nlogn)

如果一个节点是其父亲的重儿子,则他的子树必定在他的兄弟之中最多,所以任意节点到根的路径上所有重边连接的父节点在计算答案是必定不会遍历到这个节点,所以一个节点的被遍历的次数等于他到根节点路径上的轻边树 + 1 +1 +1(之所以要 + 1 +1 +1 是因为他本身要被遍历到),所以一个节点的被遍历次数 = log ⁡ n + 1 =\\log n+1 =logn+1, 总时间复杂度则为 O ( n ( log ⁡ n + 1 ) ) = O ( n log ⁡ n ) O(n(\\log n+1))=O(n\\log n) O(n(logn+1))=O(nlogn),输出答案花费 O ( m ) O(m) O(m),整体时间复杂度为 O ( n log ⁡ n ) O(n\\log n) O(nlogn)

例题

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

Weblink

https://www.luogu.com.cn/problem/CF741D

Problem

给定一棵以 1 1 1 为根的有根树,每条边的权值是 ['a','v'] 的一个字母。现对于每个树上结点 x x x ,求出结点 x x x 的子树的最长的一条 “回文” 路径的长度。“回文” 路径的含义是将路径上所有的字母取出,任意排列后可以组成一个回文串。

1 ≤ n ≤ 5 × 1 0 5 1\\le n\\le 5\\times 10^5 1n5×105

Solution

字符串重排后是回文串,显然该字符串内最多只有一种字符出现奇数次,其余均出现偶数次。

很容易可以想到异或操作。

统计字符串是否只有一个字符出现奇数次,显然可以将字符表示为二进制数,每条边一个字符,我们就可以将边权设为 2   c h − ′ a ′ 2^{\\ ch-'a'} 2 cha ,两条路的合并操作就可以 O ( 1 ) O(1) O(1) 使用 XOR \\text{XOR} XOR 来合并。我们可以根据一条路径上的异或和来判断是否是回文串。

显然对于树上两条路径之间的异或和有:

dis ( u , v ) = dis ( u , 1 ) ⊕ dis ( lca u , v , 1 ) ⊕ dis ( v , 1 ) ⊕ dis ( lca u , v , 1 ) = dis ( u , 1 ) ⊕ dis ( v , 1 ) \\begin{aligned} \\text{dis}(u,v)&=\\text{dis}(u,1)\\oplus \\text{dis}(\\text{lca}_{u,v},1)\\oplus \\text{dis}(v,1)\\oplus \\text{dis}(\\text{lca}_{u,v},1) \\\\ &=\\text{dis}(u,1)\\oplus \\text{dis}(v,1) \\end{aligned} dis(u,v)=dis(u,1)dis(lcau,v,1)dis(v,1)dis(lcau,v,1)=dis(u,1)dis(v,1)

因此,我们只需处理出每个点

以上是关于学习笔记树上启发式合并的主要内容,如果未能解决你的问题,请参考以下文章

学习笔记dsu on tree

CF600ELomsat gelral——树上启发式合并

#线段树合并树上启发式合并#CF600E Lomsat gelral

树上启发式合并/dsu on tree

CF 600 E Lomsat gelral —— 树上启发式合并

CF600ELomset gelral 题解(树上启发式合并)