dsu on tree(树上启发式合并)学习笔记

最近队友都学了这个算法,我也来凑个热闹学习一下.
Dsu on tree:目前我的理解就是一种对树上利用轻重链的性质进行子树统计的一种优化方法
因为一些问题中,需要反复清空子树的一些信息,防止其对隔壁树的兄弟信息统计进行干扰
而对于最后一颗需要进行统计的树,显然它是不用被清空的,而且它的信息在回溯时也能被其父亲使用.
那么,我们选择节点数最多的子树(重儿子)进行信息的保留,而对其他的子树(轻儿子)进行信息的清空.
在诸多dfs中,我们只需记住这一性质就能理解这个算法了,保留当前重儿子信息,清空当前节点轻儿子的信息.
这里重儿子信息的保留是相对于当前节点而言的,并不是说那颗子树不会被清空,当某个重儿子的父亲是一个轻儿子节点时,显然,这个父亲的信息被清空,这个所谓的重儿子信息也会被清空.
所以,轻重儿子信息的节点应当是基于你现在dfs进行的u节点而言的.
学习资料:
资料1
资料2
问题提出:给一个树,树上每个节点有若干个颜色,有q次询问,
每次询问 u , c o l o r u,color u,color.你需要回答以 u u u为子树,包含颜色为 c o l o r color color的树有多少.
方法引入:在一般的dfs上加上轻重儿子的定义,对于重儿子,不清空,轻儿子就清空.
由于轻重链的一些性质,每个轻儿子只会被清空 l o g n logn logn次,所以整个复杂度是 O ( n l o g n ) O(nlogn) O(nlogn)
以下是模仿文章的一个写法:

vector<int> G[maxn];int sz[maxn];
void dfs1(int u,int fa){
	sz[u]=1;
	for(auto v :G[u]){
		if(v!=fa) dfs1(v,u),sz[u]+=sz[v];
	}
}
int cnt[maxn];bool big[maxn];int color[maxn];
void add(int u,int fa,int x){
	cnt[color[u]]+=x;
	for(auto v : G[u]){
		if(v!=fa&&big[v]==false) add(v,u,x);
	}
}
ll ans[maxn];
void dfs(int u,int fa,bool keep){
	int mx = -1,bigchild=-1;
	for(auto v : G[u]){
		if(v!=fa&&sz[v]>mx) mx = sz[v],bigchild=v;
	}
	for(auto v :G[u]){
		if(v!=fa&&v!=bigchild) dfs(v,u,0);
	}
	if(bigchild!=-1) dfs(bigchild,u,1),big[bigchild]=1;
	add(u,fa,1);
	//此时,该点u为根的子树已经统计完毕,应当在这里进行问题的回答
	if(bigchild!=-1) big[bigchild]=0;
	if(keep==0) add(u,fa,-1);//如果u是轻儿子,那么需要清空信息,请注意,信息只能在此清空,当前节点为轻节点时
	//u是重儿子就不需要清空信息,因为是最后一次询问了 
}

例题1,CF600E Lomsat gelral
题意:有一棵 n 个结点的以 1 号结点为根的有根树。
每个结点都有一个颜色
如果一种颜色在以 x 为根的子树内出现次数最多,称其在以 x 为根的子树中占主导地位。显然,同一子树中可能有多种颜色占主导地位。
你的任务是对于每一个 i ∈ [ 1 , n ] i\in[1,n] i[1,n],求出以 i i i为根的子树中,占主导地位的颜色的编号和.
这是例题,显然可以套用上述的模板,要注意的点只有一点.我们说过,重儿子的信息由于是最后才dfs,所以它不用清空,换而言之,重儿子的信息需要被重复利用,所以外部变量res,mx的清空应当在keep==0的条件下清空,保证不会清空到重儿子的res和mx的信息,否则会造成错误.
代码变量名起的不好,全局变量MX,和找重儿子sz的mx冲突了.下回改改
AC代码:

#include
using namespace std;
const int maxn = 1e6+5;
const int INF = 1e9+7;
typedef long long ll;
typedef pair<int,int> pii;
#define all(a) (a).begin(), (a).end()
#define pb(a) push_back(a)
//前向星
// for(int i=head[u];i!=-1;i=nxt[i]) v = to[i]
//int nxt[maxn],head[maxn],to[maxn];// head[u],cnt 初始值是-1
//int tot = -1;
//void add(int u,int v){
//	nxt[++tot] = head[u];
//	head[u] = tot;
//	to[tot] = v;
//}
vector<int> G[maxn];int sz[maxn];
void dfs1(int u,int fa){
	sz[u]=1;
	for(auto v :G[u]){
		if(v!=fa) dfs1(v,u),sz[u]+=sz[v];
	}
}
int cnt[maxn];bool big[maxn];int color[maxn];
ll res=0,MX = 0;
void add(int u,int fa,int x){
	cnt[color[u]]+=x;
	if(x==1){
		//只在x==1时才能统计答案,
		if(MX<cnt[color[u]]) MX = cnt[color[u]] , res =color[u];
		else if(MX==cnt[color[u]]) res+=color[u]; 
	}
	for(auto v : G[u]){
		if(v!=fa&&!big[v]) add(v,u,x);
	}
}
ll ans[maxn];
void dfs(int u,int fa,bool keep){
	int mx = -1,bigchild=-1;
	for(auto v : G[u]){
		if(v!=fa&&sz[v]>mx) mx = sz[v],bigchild=v;
	}
	for(auto v :G[u]){
		if(v!=fa&&v!=bigchild) dfs(v,u,0);
	}
	if(bigchild!=-1) dfs(bigchild,u,1),big[bigchild]=1;
	add(u,fa,1);
	//此时,该点u为根的子树已经统计完毕,应当在这里进行问题的回答
	ans[u] = res;
	if(bigchild!=-1) big[bigchild]=0;
	if(keep==0) add(u,fa,-1),res = MX = 0;//如果u是轻儿子,那么需要清空信息
	//u是重儿子就不需要清空信息,因为是最后一次询问了 
}
int main(){
    ios::sync_with_stdio(false);
	cin.tie(0);
	cout.tie(0);
	int n;cin>>n;
	for(int i=1;i<=n;i++) cin>>color[i];
	for(int i=1;i<=n-1;i++){
		int u,v;cin>>u>>v;
		G[u].pb(v);G[v].pb(u);
	}
	dfs1(1,0);
	dfs(1,0,0);
	for(int i=1;i<=n;i++) cout<<ans[i]<<" ";
}

例题2CF570D Tree Requests
给定一个以 1 为根的 n n n个结点的树,每个点上有一个字母 ( a − z ) (a-z) az,每个点的深度定义为该节点到 1号结点路径上的点数。每次询问 a , b 查询以 a a, b 查询以a a,b查询以a为根的子树内深度为 b b b 的结点上的字母重新排列之后是否能构成回文串。
思路:思考回文的性质,每个字母要么出现偶数次,至多有一个字母出现奇数次.
所以我们只需要检查以 a a a节点为子树,深度为 b b b节点奇数次数节点是否大于1.
另外,dsu on tree实际上只进行了两次dfs,所以需要在dfs过程中一口气把问题全部解决.
也就是离线处理,事先把询问存到一个 v e c t o r < p a i r < i n t , i n t > > q vector> q vector<pair<int,int>>q里面
然后在dfs过程中,我们处理完 a d d ( u , f a , 1 ) add(u,fa,1) add(u,fa,1)后,得到了 u u u为子树的所有信息,把 q [ u ] q[u] q[u]的询问全部解决掉.

#include
using namespace std;
const int maxn = 1e6+5;
const int INF = 1e9+7;
typedef long long ll;
typedef pair<int,int> pii;
#define all(a) (a).begin(), (a).end()
#define pb(a) push_back(a)
//前向星
// for(int i=head[u];i!=-1;i=nxt[i]) v = to[i]
//int nxt[maxn],head[maxn],to[maxn];// head[u],cnt 初始值是-1
//int tot = -1;
//void add(int u,int v){
//	nxt[++tot] = head[u];
//	head[u] = tot;
//	to[tot] = v;
//}
vector<int> G[maxn];int sz[maxn];int depth[maxn];
void dfs1(int u,int fa){
	sz[u]=1;depth[u] = depth[fa]+1;
	for(auto v :G[u]){
		if(v!=fa) dfs1(v,u),sz[u]+=sz[v];
	}
}
int cnt[maxn][26];bool big[maxn];int color[maxn];
int ch[maxn];
void add(int u,int fa,int x){
	cnt[depth[u]][ch[u]]+=x;
	for(auto v : G[u]){
		if(v!=fa&&big[v]==false) add(v,u,x);
	}
}
bool check(int dep){
	//检查以u节点为根的子树,深度为dep是否有大于1的奇数次字母出现
	int res = 0;
	for(int i=0;i<26;i++){
		if(cnt[dep][i]%2==1) res++;
	}
	return res<=1;
}
bool ans[maxn];vector<pii> q[maxn];
void dfs(int u,int fa,bool keep){
	int mx = -1,bigchild=-1;
	for(auto v : G[u]){
		if(v!=fa&&sz[v]>mx) mx = sz[v],bigchild=v;
	}
	for(auto v :G[u]){
		if(v!=fa&&v!=bigchild) dfs(v,u,0);
	}
	if(bigchild!=-1) dfs(bigchild,u,1),big[bigchild]=1;
	add(u,fa,1);
	//此时,该点u为根的子树已经统计完毕,应当在这里进行问题的回答
	for(auto [dep,id] : q[u]){
		ans[id] = check(dep);
	}
	if(bigchild!=-1) big[bigchild]=0;
	if(keep==0) add(u,fa,-1);//如果u是轻儿子,那么需要清空信息,请注意,信息只能在此清空,当前节点为轻节点时
	//u是重儿子就不需要清空信息,因为是最后一次询问了
}
int main(){
    ios::sync_with_stdio(false);
	cin.tie(0);
	cout.tie(0);
	int n,m;cin>>n>>m;
	for(int i=2,x;i<=n;i++){
		cin>>x;G[i].pb(x);G[x].pb(i);
	}
	for(int i=1;i<=n;i++){
		char tmp;cin>>tmp;
		ch[i] = tmp-'a';
	}
	for(int i=1;i<=m;i++){
		int a,b;cin>>a>>b;
		q[a].push_back({b,i});
	}
	dfs1(1,0);
	dfs(1,0,0);
	for(int i=1;i<=m;i++){
		cout<< (ans[i] ? "Yes":"No" )<<"\n";
	}
}

例题3 CF375D Tree and Queries
dsu on tree(树上启发式合并)学习笔记_第1张图片
和例题1差不多,不过是把例题2的技巧结合下,操作作一个离线的处理就好
需要注意的点有两点
1.由于往下统计的点出现次数是递增的,每当经过一个点的时候,除了清空轻儿子操作时,其cnt[color[u]]都是递增的
所以用num来记录cnt[color[u]]出现的次数,num[k]就是大于等于k颜色的种类数目?
为什么,因为大于k次数的颜色必然会经过num[cnt[color[u]]++这一步骤,而且之后也不会再经过了,保证了不会一种颜色统计多次
2.在清空轻儿子的时候,与统计相反,需要先进行num数组的减少,再进行cnt数组的减少.
因为我们num数组要保存的是当前颜色出现的次数,如果cnt先减了,就会错误地减少了(差一个)

#include
using namespace std;
const int maxn = 1e6+5;
const int INF = 1e9+7;
typedef long long ll;
typedef pair<int,int> pii;
#define all(a) (a).begin(), (a).end()
#define pb(a) push_back(a)
//前向星
// for(int i=head[u];i!=-1;i=nxt[i]) v = to[i]
//int nxt[maxn],head[maxn],to[maxn];// head[u],cnt 初始值是-1
//int tot = -1;
//void add(int u,int v){
//	nxt[++tot] = head[u];
//	head[u] = tot;
//	to[tot] = v;
//}
vector<int> G[maxn];int sz[maxn];
vector<pii> q[maxn];
void dfs1(int u,int fa){
	sz[u]=1;
	for(auto v :G[u]){
		if(v!=fa) dfs1(v,u),sz[u]+=sz[v];
	}
}
int cnt[maxn],num[maxn];bool big[maxn];int color[maxn];
void add(int u,int fa,int x){
	if(x==1){
	cnt[color[u]]+=x;
	num[cnt[color[u]]]+=x;
	}
	else{
		num[cnt[color[u]]]+=x;
		cnt[color[u]]+=x;
	}
	for(auto v : G[u]){
		if(v!=fa&&big[v]==false) add(v,u,x);
	}
}
ll ans[maxn];
void dfs(int u,int fa,bool keep){
	int mx = -1,bigchild=-1;
	for(auto v : G[u]){
		if(v!=fa&&sz[v]>mx) mx = sz[v],bigchild=v;
	}
	for(auto v :G[u]){
		if(v!=fa&&v!=bigchild) dfs(v,u,0);
	}
	if(bigchild!=-1) dfs(bigchild,u,1),big[bigchild]=1;
	add(u,fa,1);
	//此时,该点u为根的子树已经统计完毕,应当在这里进行问题的回答
	for(auto [k,id] : q[u]){
		ans[id] = num[k];
	}
	if(bigchild!=-1) big[bigchild]=0;
	if(keep==0) add(u,fa,-1);//如果u是轻儿子,那么需要清空信息,请注意,信息只能在此清空,当前节点为轻节点时
	//u是重儿子就不需要清空信息,因为是最后一次询问了 
}
int main(){
    ios::sync_with_stdio(false);
	cin.tie(0);
	cout.tie(0);
	int n,m;cin>>n>>m;
	for(int i=1;i<=n;i++){
		cin>>color[i];
	}
	for(int i=1;i<=n-1;i++){
		int u,v;cin>>u>>v;
		G[u].pb(v);G[v].pb(u);
	}
	for(int i=1;i<=m;i++){
		int u,k;cin>>u>>k;
		q[u].push_back({k,i});
	}
	dfs1(1,0);
	dfs(1,0,0);
	for(int i=1;i<=m;i++){
		cout<<ans[i]<<"\n";
	}
}

例题4CF1709E XOR Tree
题意描述:
dsu on tree(树上启发式合并)学习笔记_第2张图片
思路:树上的异或需要想到根到自己的一些性质(LCA).经过一些摸索我们发现,
如果有两个点的路径异或和为0,我们先设 d u d_u du为1到 u u u的路径异或和.
我们会发现,对于任意两点 u , v u,v u,v的路径异或,可以化为 d u x o r d v x o r A l c a ( u , v ) d_u xor {d_v}xorA_{lca(u,v)} duxordvxorAlca(u,v), A l c a ( u , v ) 是 u , v 公共祖先的点权 A_{lca(u,v)}是u,v公共祖先的点权 Alca(u,v)u,v公共祖先的点权
那这又有何用呢?注意点权可以修改成任意值,我们设想一下,如果让 a u a_u au修改为极大值后,假设两点 a , b , l c a ( a , b ) = u a,b,lca(a,b)=u a,b,lca(a,b)=u,那么 d a x o r d b x o r A u = 一个极大值 d_axord_bxorA_u=一个极大值 daxordbxorAu=一个极大值,显然这个极大值不可能在原本的点权里面出现,只要你赋值得当,那么以 u u u为根的子树,包含 u u u的路径都是合法情况,更进一步,修改后u的子树上的每一个点v,
d v d_v dv都异或上了一个极大值(因为1->v的路径必然包含u这个点,因为v是u子树里面的一个点),
那么这些 d v d_v dv显然不用再考虑了,没有人异或上他们再异或上LCA会变成0
那到这里已经有一些头绪了,我们开一个 s e t , S [ u ] set,S[u] set,S[u]来记录 以 u 为根的子树 , 包含的合法 d u 的集合 以u为根的子树,包含的合法d_u的集合 u为根的子树,包含的合法du的集合
什么是合法的 d u d_u du,就是上面没有被极大值异或过的 d u d_u du.
那么什么时候 u u u认为要修改呢,就是出现它两颗子树分别有两个孩子 a , b a,b a,b, d a x o r d b = = a [ u ] d_axord_b==a[u] daxordb==a[u].
那我们就挨个扫吧,每次去遍历一个子树的合集 S [ v ] S[v] S[v],不断地检查是否有 S [ v ] x o r a [ u ] S[v]xora[u] S[v]xora[u]在之前的集合中出现,并且扫完一颗子树后,再遍历这个子树把它插入到 S [ u ] S[u] S[u]集合中去.
但明眼人都能看出问题,你这不对吧,这样做最坏不就是每个节点都被父亲节点插入进去,一个元素甚至可能被插入N次,扫描集合也很花费时间,大概率是TLE/MLE的
没错,所以引入Dsu on tree.我们发现在扫描第一颗子树的时候,不会产生答案(除了u点直接到第一颗子树某个节点路径异或为0外,这个情况我们进行特判).那么我们可以直接保留这个子树的信息,而不是单纯地把集合从 S [ v ] S[v] S[v]取出再放入到另外一个集合 S [ u ] S[u] S[u]中,我们将直接使用 s w a p ( S [ u ] , S [ v ] ) swap(S[u],S[v]) swap(S[u],S[v])函数,它是O(1)的,而且保留了大量的空间.之前学了那么多例题,我们已经知道,这个要保留的子树,就是当前节点 u u u的重儿子所在的子树的信息.
此外,如果出现了需要将 u u u点赋值为极大值的情况,事实上就是清空 S [ u ] S[u] S[u]集合,之前已经证明过了,这些节点不会再后续向上统计的时候影响答案,所以就把 S [ u ] S[u] S[u]集合清空认为没有影响
这样保证了时间的复杂度不会太离谱,怎么估算本人学艺不精就不会了

#include
using namespace std;
const int maxn = 2e5+5;
const int INF = 1e9+7;
typedef long long ll;
typedef pair<int,int> pii;
#define all(a) (a).begin(), (a).end()
#define pb(a) push_back(a)
//前向星
// for(int i=head[u];i!=-1;i=nxt[i]) v = to[i]
//int nxt[maxn],head[maxn],to[maxn];// head[u],cnt 初始值是-1
//int tot = -1;
//void add(int u,int v){
//	nxt[++tot] = head[u];
//	head[u] = tot;
//	to[tot] = v;
//}
vector<int> G[maxn];int sz[maxn];int d[maxn];
int a[maxn];
void dfs1(int u,int fa){
	sz[u]=1;d[u] = d[fa] ^ a[u];
	for(auto v :G[u]){
		if(v!=fa) dfs1(v,u),sz[u]+=sz[v];
	}
}
bool big[maxn];
set<int> S[maxn];//S[u]:以u为根的仍然合法的du的点集合
int ans=0;
void dfs(int u,int fa,bool keep){
	int mx = -1,bigchild=-1;
	for(auto v : G[u]){
		if(v!=fa&&sz[v]>mx) mx = sz[v],bigchild=v;
	}
	for(auto v :G[u]){
		if(v!=fa&&v!=bigchild) dfs(v,u,0);
	}
	if(bigchild!=-1) dfs(bigchild,u,1),big[bigchild]=1;
	if(bigchild!=-1) swap(S[bigchild],S[u]);
	bool ok = false;
	if(S[u].count(a[u]^d[u])) ok = true;
	S[u].insert(d[u]);
	for(auto v :G[u]){
		for(auto s : S[v]){
			if(S[u].count(s^a[u])) {
				ok = true;break;
			}
		}
		if(ok) break;
		if(!ok) for(auto s : S[v]) S[u].insert(s);
	}
	if(ok) ans++,S[u].clear();
	//此时,该点u为根的子树已经统计完毕,应当在这里进行问题的回答
	if(bigchild!=-1) big[bigchild]=0;
}
int main(){
    ios::sync_with_stdio(false);
	cin.tie(0);
	cout.tie(0);
	int n;cin>>n;
	for(int i=1;i<=n;i++){
		cin>>a[i];
	}
	for(int i=1,u,v;i<=n-1;i++){
		cin>>u>>v;G[u].pb(v);G[v].pb(u);
	}
	dfs1(1,0);
	dfs(1,0,0);
	cout<<ans;
}

例题5F. Dominant Indices

dsu on tree(树上启发式合并)学习笔记_第3张图片
似乎与前面的例题1例题2类似,套模板水过去就行

#include
using namespace std;
const int maxn = 1e6+5;
const int INF = 1e9+7;
typedef long long ll;
typedef pair<int,int> pii;
#define all(a) (a).begin(), (a).end()
#define pb(a) push_back(a)
//前向星
// for(int i=head[u];i!=-1;i=nxt[i]) v = to[i]
//int nxt[maxn],head[maxn],to[maxn];// head[u],cnt 初始值是-1
//int tot = -1;
//void add(int u,int v){
//	nxt[++tot] = head[u];
//	head[u] = tot;
//	to[tot] = v;
//}
vector<int> G[maxn];int sz[maxn];int depth[maxn];
void dfs1(int u,int fa){
	sz[u]=1;depth[u] = depth[fa] + 1;
	for(auto v :G[u]){
		if(v!=fa) dfs1(v,u),sz[u]+=sz[v];
	}
}
int cnt[maxn];bool big[maxn];int color[maxn];
int dis=0,num=0;
void add(int u,int fa,int x){
	cnt[depth[u]]+=x;
	if(x==1){
	if(cnt[depth[u]]>num) num = cnt[depth[u]],dis = depth[u];
	else if(cnt[depth[u]]==num) dis = min(dis,depth[u]);
	}
	for(auto v : G[u]){
		if(v!=fa&&big[v]==false) add(v,u,x);
	}
}
ll ans[maxn];
void dfs(int u,int fa,bool keep){
	int mx = -1,bigchild=-1;
	for(auto v : G[u]){
		if(v!=fa&&sz[v]>mx) mx = sz[v],bigchild=v;
	}
	for(auto v :G[u]){
		if(v!=fa&&v!=bigchild) dfs(v,u,0);
	}
	if(bigchild!=-1) dfs(bigchild,u,1),big[bigchild]=1;
	add(u,fa,1);
	//此时,该点u为根的子树已经统计完毕,应当在这里进行问题的回答
	ans[u] = dis - depth[u];
	if(bigchild!=-1) big[bigchild]=0;
	if(keep==0) add(u,fa,-1),dis = 0,num = 0;//如果u是轻儿子,那么需要清空信息,请注意,信息只能在此清空,当前节点为轻节点时
	//u是重儿子就不需要清空信息,因为是最后一次询问了 
}
int main(){
    ios::sync_with_stdio(false);
	cin.tie(0);
	cout.tie(0);
	int n;cin>>n;
	for(int i=1;i<=n-1;i++){
		int u,v;cin>>u>>v;
		G[u].pb(v);G[v].pb(u);
	}
	dfs1(1,0);
	dfs(1,0,0);
	for(int i=1;i<=n;i++) cout<<ans[i]<<"\n";
}

你可能感兴趣的:(学习,深度优先,算法)