今天来讲一下树上并发式合并。
灵感来源于我在看蓝桥杯历年真题是看到的一道题目
[蓝桥杯 2023 省 A] 颜色平衡树
那么这就是一道树上并发式合并的模版
先来讲一下树上并发式合并的概念。
首先我们需要知道重儿子这个知识点重链剖分,这个是我之前写的博文
那么第零步,求出重儿子。
第一步,遍历所有轻儿子,并求出其轻儿子子树的答案,但不记录其贡献。
第二步,遍历所有重儿子,并求出其重儿子子树的答案和贡献。
第三步,从当前子树开始,计算包含当前节点以及所有轻儿子的贡献
第四步,记录答案
第五步,若当前点为轻儿子,擦除贡献
实际上,这便是树上并发式合并的模版,然而我在学习时感到十分晦涩,故我以这道例题来详细剖析这个算法。
针对这道例题,我们发现,对于每个节点u,我们用cnt[c[u]]来表示颜色的出现次数,用ccnt[cnt[c[u]]]来表示颜色出现次数为cnt[c[u]]的出现次数
什么意思呢?
以原题样例举例
对于一号节点,2号颜色出现了2次,而出现2次的种类共有3种
于是我们发现,2*3=6,正好等于其子树大小
因此经过思考,我们可以得到一个结论,那便是当cnt[c[u]]*ccnt[cnt[c[u]]]=siz[u]时,这个节点满足答案
那么我们便依照模版构思一下代码
#include<bits/stdc++.h>
using namespace std;
int c[200005],f[200005];
vector<int>g[200005];
int hson[200005];
int siz[200005];
int cnt[200005];
int ccnt[200005];
int ans=0;
int son;
void add(int x,int pt)
{
ccnt[cnt[c[x]]]--;
cnt[c[x]]+=pt;
ccnt[cnt[c[x]]]++;
for(int i=0;i<g[x].size();i++)
{
int v=g[x][i];
if(v!=son)//不是重儿子
{
add(v,pt);
}
}
}
void dfs1(int x)
{
siz[x]=1;
for(int i=0;i<g[x].size();i++)
{
int v=g[x][i];
dfs1(v);
siz[x]+=siz[v];
if(siz[hson[x]]<siz[v])
{
hson[x]=v;
}
}
}
void dfs2(int x,bool t)
{
for(int i=0;i<g[x].size();i++)
{
int v=g[x][i];
if(v!=hson[x])//遍历轻儿子
{
dfs2(v,false);
}
}
if(hson[x])//查询重儿子
{
dfs2(hson[x],true);
}
son=hson[x];
add(x,1);//从当前点开始计算包含当前点以及所有轻儿子的贡献
if(ccnt[cnt[c[x]]]*cnt[c[x]]==siz[x])//记录答案
{
ans++;
}
son=0;//记得将重儿子标记清空
if(t==false)
{
add(x,-1);//删除轻儿子贡献
}
}
int main()
{
int n;
cin>>n;
int root;
for(int i=1;i<=n;i++)
{
cin>>c[i]>>f[i];
if(f[i])
{
g[f[i]].push_back(i);
}
}
dfs1(1);
dfs2(1,true);
cout<<ans<<endl;
return 0;
}
这便是上题的正确代码
接下来来给大家再详细介绍一下这个算法的核心
从上题我们已经了解到,当我们要看到有关树上记录颜色的题目时,我们优先考虑的就是树上启发式合并。
接下来我们来介绍另外一道比较经典的题目
题目大意为,你有一个以顶点1为根的树。每个顶点都被染上了某种颜色。如果颜色c在顶点v的子树中出现的次数不少于其他任何颜色,则称颜色c在顶点v的子树中占主导地位。因此,某个顶点的子树中可能有两个或更多的颜色占主导地位。顶点v的子树是指顶点v以及所有包含顶点v的路径到根的所有其他顶点。对于每个顶点v,找出其子树中所有主导颜色的总和
那么我们只需套用模版即可,这里我就不在打代码,大家可以自行去练习来加深理解
希望能对大家有所帮助