浅谈树上启发式合并
\(\quad\)在 \(51\) nod 上做了一道毒瘤题,顺便学了树上启发式合并的算法(又叫 Dcu on Tree、静态链分治),接下来我会先概述树上启发式合并的基本知识,然后结合部分题目讲解,可能会写的很长,做好心理准备。
\(\quad\)学这个之前需要对树上操作、 \(dfs\) 序和轻重链剖分等知识有一定了解,最好已经掌握了树链剖分。
\(\quad\)树上启发式合并 (Dsu on Tree),是一个在 \(O(n\log n)\) 时间内解决许多树上问题的有力算法,对于某些树上离线问题可以速度大于等于大部分算法且更易于理解和实现。
\(\quad\)虽然这个算法不能完成修改操作,只能完成询问操作,但还是很受欢迎,在树上离线问题中独占鳌头,毕竟其他算法(例如树上莫队)时间复杂度都是 \(O(n\sqrt{n})\) 。
\(\quad\)题目链接:CF600E Lomsat gelral(洛谷的链接)
题目描述
-
有一棵 \(n\) 个结点的以 \(1\) 号结点为根的有根树。
-
每个结点都有一个颜色,颜色是以编号表示的, \(i\) 号结点的颜色编号为 \(c_i\)
-
如果一种颜色在以 \(x\) 为根的子树内出现次数最多,称其在以 \(x\) 为根的子树中占主导地位。显然,同一子树中可能有多种颜色占主导地位。
-
你的任务是对于每一个 \(i\in[1,n]\) ,求出以 \(i\) 为根的子树中,占主导地位的颜色的编号和。
-
\(n\le 10^5,c_i\le n≤10^5,c_i≤n\)
\(\quad\)先想一下暴力算法,对于每一次询问都遍历整棵子树,然后统计答案,最后再清空cnt数组,最坏情况是时间复杂度为 \(O(n^2)\) ,对于 \(10^5\) 的数据肯定是过不去的。
\(\quad\)现在考虑优化算法,暴力算法跑得慢的原因就是多次遍历,多次清空数组,一个显然的优化就是将询问同一个子树的询问放在一起处理,但这样还是没有处理到关键,最坏情况时间复杂度还是 \(O(n^2)\) ,考虑到询问 \(x\) 节点时, \(x\) 的子树对答案有贡献,所以可以不用清空数组,先统计 \(x\) 的子树中的答案,再统计 \(x\) 的答案,这样就需要提前处理好 \(dfs\) 序。
\(\quad\)然后我们可以考虑一个优化,遍历到最后一个子树时是不用清空的,因为它不会产生对其他节点影响了,根据贪心的思想我们当然要把节点数最多的子树(即重儿子形成的子树)放在最后,之后我们就有了一个看似比较快的算法,先遍历所有的轻儿子节点形成的子树,统计答案但是不保留数据,然后遍历重儿子,统计答案并且保留数据,最后再遍历轻儿子以及父节点,合并重儿子统计过的答案。
\(\quad\)其实树上启发式合并的基本思路就是这样,可以看一下代码理解。
il int check(int x)//统计答案
{
int num=0,ret=0;
for(re i=1;i<=n;i++)
{
if(cnt[i]==num){ret+=i;}
else if(cnt[i]>num){num=cnt[i],ret=i;}
}
return ret;
}
il void add(int x){cnt[col[x]]++;}//单点增加
il void del(int x){cnt[col[x]]--;}//单点减少
il void raise(int x){for(re i=seg[x];i<=seg[x]+size[x]-1;i++)add(rev[i]);}//增加x子树的贡献
il void clear(int x){for(re i=seg[x];i<=seg[x]+size[x]-1;i++)del(rev[i]);}//清空x子树的贡献
il void dfs1(int x,int fa)
{
dep[x]=dep[fa]+1;father[x]=fa;//处理深度,父亲
seg[x]=++seg[0];rev[seg[x]]=x;size[x]=1;//子树大小,dfs序
for(re i=head[x],y;i,y=go[i];i=next[i])
{
if(y==fa)continue;dfs1(y,x);
size[x]+=size[y];
if(size[y]>size[son[x]])son[x]=y;//重儿子
}
}
il void dfs2(int x,int flag)//flag表示是否为重儿子,1表示重儿子,0表示轻儿子
{
for(re i=head[x],y;i,y=go[i];i=next[i])
{
if(y==son[x]||y==father[x])continue;
dfs2(y,0);//先遍历轻儿子
}
if(son[x])dfs2(son[x],1);//再遍历重儿子
for(re i=head[x],y;i,y=go[i];i=next[i])
{
if(y==son[x]||y==father[x])continue;
raise(y);//更新轻儿子的贡献
}add(x);//加上x结点本身的贡献
ans[x]=check(x);//更新答案
if(!flag)clear(x);//如果是轻儿子,就清空
}
\(\quad\)这样时间复杂度就可以变成 \(O(n\log n)\) 的,但还是要考虑严谨证明一下。
\(\quad\)我们像树链剖分一样定义重边和轻边(连向重儿子的为重边,其余为轻边),对于一棵有 \(n\) 个节点的树:
\(\quad\)根节点到树上任意节点的轻边数不超过 \(\log n\) 条。我们设根到该节点有 \(x\) 条轻边该节点的子树大小为 \(y\) ,显然轻边连接的子节点的子树大小小于父亲的一半(若大于一半就不是轻边了),则 \(y<n/2^x\) ,显然 \(n>2^x\) ,所以 \(x<\log n\)。
\(\quad\)又因为如果一个节点是其父亲的重儿子,则他的子树必定在他的兄弟之中最多,所以任意节点到根的路径上所有重边连接的父节点在计算答案是必定不会遍历到这个节点,所以一个节点的被遍历的次数等于他到根节点路径上的轻边树 \(+1\)(之所以要 \(+1\) 是因为他本身要被遍历到),所以一个节点的被遍历次数\(=\log n+1\) ,总时间复杂度则为 \(O(n(\log n+1))=O(n\log n)\) ,输出答案花费 \(O(m)\) 。
\(\quad\)现在理解了基本思路后,再回到模板题看一看,大体思路是一样的,只有统计答案的地方有些许不同,不知怎的我使用了线段树来维护答案,每次单点修改,统计答案时只要记录 \(Max[1].num\) 即可,用线段树维护貌似加了时间复杂度 \(O(n\log ^2 n)\) 。
#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
using namespace std;
#define re register int
#define int long long
#define il inline
#define next nee
il int read()
{
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)&&ch!='-')ch=getchar();
if(ch=='-')f=-1,ch=getchar();
while(isdigit(ch))x=(x<<1)+(x<<3)+ch-'0',ch=getchar();
return x*f;
}
il void print(int x)
{
if(x<0)putchar('-'),x=-x;
if(x/10)print(x/10);
putchar(x%10+'0');
}
const int N=1e5+5;
int n,m,next[N<<1],father[N],son[N],go[N<<1],head[N],tot,seg[N],rev[N];
int dep[N],size[N],col[N],ans[N];
il void Add(int x,int y)
{
next[++tot]=head[x];
head[x]=tot;go[tot]=y;
}
struct node{
int num,tot;//num表示编号大小,tot表示数量
}Max[N<<2];
il void build(int k,int l,int r)//建树
{
if(l==r){Max[k].num=l;return;}//初始化为编号
int mid=l+r>>1;
build(k<<1,l,mid);build(k<<1|1,mid+1,r);
}
il void change(int k,int l,int r,int x,int y)
{
if(l==r){Max[k].tot+=y;return;}
int mid=l+r>>1;
if(x<=mid)change(k<<1,l,mid,x,y);
else change(k<<1|1,mid+1,r,x,y);
if(Max[k<<1].tot>Max[k<<1|1].tot)Max[k]=Max[k<<1];//左儿子数量多
else if(Max[k<<1].tot<Max[k<<1|1].tot)Max[k]=Max[k<<1|1];//右儿子数量多
else Max[k].tot=Max[k<<1].tot,Max[k].num=Max[k<<1].num+Max[k<<1|1].num;//一样多时,编号相加
}
il void add(int x){change(1,1,n,col[x],1);}
il void del(int x){change(1,1,n,col[x],-1);}
il void raise(int x){for(re i=seg[x];i<=seg[x]+size[x]-1;i++)add(rev[i]);}
il void clear(int x){for(re i=seg[x];i<=seg[x]+size[x]-1;i++)del(rev[i]);}
il void dfs1(int x,int fa)
{
dep[x]=dep[fa]+1;father[x]=fa;seg[x]=++seg[0];rev[seg[x]]=x;size[x]=1;
for(re i=head[x],y;i,y=go[i];i=next[i])
{
if(y==fa)continue;dfs1(y,x);
size[x]+=size[y];
if(size[y]>size[son[x]])son[x]=y;
}
}
il void dfs2(int x,int flag)
{
for(re i=head[x],y;i,y=go[i];i=next[i])
{
if(y==son[x]||y==father[x])continue;
dfs2(y,0);
}
if(son[x])dfs2(son[x],1);
for(re i=head[x],y;i,y=go[i];i=next[i])
{
if(y==son[x]||y==father[x])continue;
raise(y);
}add(x);
ans[x]=Max[1].num;
if(!flag)clear(x);
}
signed main()
{
n=read();
for(re i=1;i<=n;i++)col[i]=read();//记录每个点的颜色
for(re i=1,x,y;i<n;i++)x=read(),y=read(),Add(x,y),Add(y,x);
build(1,1,n);dfs1(1,0);dfs2(1,1);//预处理
for(re i=1;i<=n;i++)print(ans[i]),putchar(' ');
return 0;
}
\(\quad\)题目链接:CF570D Tree Requests(洛谷的链接)
思路:
\(\quad\) Dsu on Tree模板题,每次用 \(cnt_{i,j}\) 数组记录深度为 \(i\) 中颜色 \(j\) 的出现情况,因为要构成回文,所以最多只能有一种字符出现次数为奇数。
#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
#include<queue>
using namespace std;
#define re register int
#define LL long long
#define il inline
#define pc putchar('\n')
#define next nee
#define inf 1e18
il int read()
{
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)&&ch!='-')ch=getchar();
if(ch=='-')f=-1,ch=getchar();
while(isdigit(ch))x=(x<<1)+(x<<3)+ch-'0',ch=getchar();
return x*f;
}
il void print(int x)
{
if(x<0)putchar('-'),x=-x;
if(x/10)print(x/10);
putchar(x%10+'0');
}
const int N=5e5+5;
int n,m,next[N],head[N],go[N],tot,s[N],father[N],cnt[N][28];
int dep[N],size[N],son[N],seg[N],rev[N];
bool ans[N];
struct node{
int x,y;};
vector<node>q[N];
il void Add(int x,int y)
{
next[++tot]=head[x];
head[x]=tot;go[tot]=y;
}
il void dfs1(int x,int fa)
{
dep[x]=dep[fa]+1;size[x]=1;seg[x]=++seg[0];rev[seg[x]]=x;
for(re i=head[x],y;i,y=go[i];i=next[i])
{
dfs1(y,x);size[x]+=size[y];
if(size[y]>size[son[x]])son[x]=y;//重儿子
}
}
il bool check(int x)//统计答案
{
int ret=0;
for(re i=1;i<=26;i++)if(cnt[x][i]&1)ret++;
return ret<=1;
}
il void add(int x){cnt[dep[x]][s[x]]++;}//单点增加
il void update(int x){for(re i=seg[x];i<=seg[x]+size[x]-1;i++)add(rev[i]);}//增加x子树的贡献
il void del(int x){cnt[dep[x]][s[x]]=0;}//单点减少
il void out(int x){for(re i=seg[x];i<=seg[x]+size[x]-1;i++)del(rev[i]);}//清空x子树的贡献
il void dfs2(int x,int flag)//flag表示是否为重儿子,1表示重儿子,0表示轻儿子
{
for(re i=head[x],y;i,y=go[i];i=next[i])
{
if(y==son[x])continue;
dfs2(y,0);//先遍历轻儿子
}
if(son[x])dfs2(son[x],1);//再遍历重儿子
for(re i=head[x],y;i,y=go[i];i=next[i])
{
if(y==son[x])continue;
update(y);//更新轻儿子的贡献
}add(x);//加上x结点本身的贡献
for(re i=0;i<q[x].size();i++)
ans[q[x][i].y]=check(q[x][i].x);//更新答案
if(!flag)out(x);//如果是轻儿子,就清空
}
signed main()
{
n=read();m=read();
for(re i=2;i<=n;i++){re x=read();father[i]=x,Add(x,i);}
string ss;cin>>ss;
for(re i=1;i<=n;i++)s[i]=ss[i-1]-'a'+1;
for(re i=1,x,y;i<=m;i++)x=read(),y=read(),q[x].push_back((node){y,i});//vector存储询问,将询问同一颗子树的放一起
dfs1(1,0);dfs2(1,0);
for(re i=1;i<=m;i++)ans[i]?puts("Yes"):puts("No");
return 0;
}
\(\quad\)题目链接:CF208E Blood Cousins(洛谷的链接)
\(\quad\)这题的思路几乎和上一道题一样,只需要稍微修改一下,因为是找一个点与多少个点拥有共同的 \(K\) 级祖先,那么我们就可以先把它的K级祖先找出来(使用倍增),然后在找这个点有几个 \(K\) 级后代,或者用深度表示成 \(dep_x+k\) ,注意这个图不是连通的,每次要清空数组。
#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
#include<queue>
using namespace std;
#define re register int
#define int long long
#define LL long long
#define il inline
#define next nee
#define inf 1e18
il int read()
{
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)&&ch!='-')ch=getchar();
if(ch=='-')f=-1,ch=getchar();
while(isdigit(ch))x=(x<<1)+(x<<3)+ch-'0',ch=getchar();
return x*f;
}
il void print(int x)
{
if(x<0)putchar('-'),x=-x;
if(x/10)print(x/10);
putchar(x%10+'0');
}
const int N=1e5+5;
int n,m,next[N],go[N],head[N],tot,father[N][20],ans[N],dep[N],son[N],seg[N],rev[N],size[N],cnt[N];
struct node{int k,id;};
vector<node>q[N];
il int Max(int x,int y){return x>y?x:y;}//求较大值
il void Add(int x,int y)//链式前向新
{next[++tot]=head[x];head[x]=tot;go[tot]=y;}
il void add(int x){cnt[dep[x]]++;}//单点增加
il void raise(int x){for(re i=seg[x];i<=seg[x]+size[x]-1;i++)add(rev[i]);}//算上x子树的贡献
il void del(int x){cnt[dep[x]]=0;}//单点减少
il void clear(int x){for(re i=seg[x];i<=seg[x]+size[x]-1;i++)del(rev[i]);}//清空x子树
il void dfs1(int x,int fa)
{
dep[x]=dep[fa]+1;size[x]=1;seg[x]=++seg[0];rev[seg[x]]=x;
for(re i=1;i<=18;i++)father[x][i]=father[father[x][i-1]][i-1];//倍增
for(re i=head[x],y;i,y=go[i];i=next[i])
{
dfs1(y,x);size[x]+=size[y];
if(size[y]>size[son[x]])son[x]=y;
}
}
il void dfs2(int x,int flag)
{
for(re i=head[x],y;i,y=go[i];i=next[i])
{
if(y==son[x])continue;
dfs2(y,0);//先遍历轻儿子
}
if(son[x])dfs2(son[x],1);//再遍历重儿子
for(re i=head[x],y;i,y=go[i];i=next[i])
{if(y==son[x])continue;raise(y);}//更新轻儿子的贡献
add(x);//加上x结点本身的贡献
for(re i=0;i<q[x].size();i++)
ans[q[x][i].id]=cnt[dep[x]+q[x][i].k];//更新答案
if(!flag)clear(x);//如果是轻儿子,就清空
}
il int find_father(int x,int y)//找x的第y级祖先
{
for(re i=18;i>=0;i--){if(y>=(1<<i))y-=(1<<i),x=father[x][i];}
return x;
}
signed main()
{
n=read();
for(re i=1,x;i<=n;i++)x=read(),father[i][0]=x,Add(x,i);
for(re i=1;i<=n;i++)if(father[i][0]==0)dfs1(i,0);//预处理,倍增数组、dfs序等树上信息
m=read();
for(re i=1,x,y,z;i<=m;i++){x=read(),y=read(),z=find_father(x,y);if(z)q[z].push_back((node){y,i});}
for(re i=1;i<=n;i++)if(father[i][0]==0)dfs2(i,0);//找每棵树的根节点,0表示轻儿子,这样不用手动清空数组
for(re i=1;i<=m;i++)print(Max(ans[i]-1,0)),putchar(' ');//注意输出要减一,要去除询问节点
return 0;
}
\(\quad\)题目链接:CF246E Blood Cousins Return(洛谷的链接)
\(\quad\)一道 \(Dcu\) 模板题,只需要用一个 \(map\) 数组来维护这个名字(字符串)是否出现过,用 \(set\) 也可以,貌似会慢一些,用 \(cnt\) 数组来维护每一层的不同名字的数量即可,因为是一个森林,所以记得要清空数组。
一个大坑点:
\(\quad\) \(WA\)第 \(50\) 个点的注意了,在储存询问时一定要判断询问这个第 \(k\) 级儿子的深度是否超过了 \(10^5\) ,因为最多只有 \(10^5\) 个点,如果超过就不用存储,答案一定是 \(0\) ,存储的话会溢出数组,遇到一些毒瘤数据就会WA。
#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
#include<map>
#include<vector>
using namespace std;
#define re register int
#define int long long
#define LL long long
#define il inline
#define next nee
#define inf 1e18
il int read()
{
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)&&ch!='-')ch=getchar();
if(ch=='-')f=-1,ch=getchar();
while(isdigit(ch))x=(x<<1)+(x<<3)+ch-'0',ch=getchar();
return x*f;
}
il void print(int x)
{
if(x<0)putchar('-'),x=-x;
if(x/10)print(x/10);
putchar(x%10+'0');
}
const int N=1e5+5;
int n,m,next[N],go[N],head[N],tot,father[N],ans[N];
int dep[N],son[N],seg[N],rev[N],size[N],cnt[N];
struct node{int k,id;};
string s[N];
vector<node>q[N];
map<string,bool>c[N];
il void Add(int x,int y)
{
next[++tot]=head[x];
head[x]=tot;go[tot]=y;
}
il void add(int x)//单点增加
{
if(!c[dep[x]][s[x]])c[dep[x]][s[x]]=1,cnt[dep[x]]++;
}
il void raise(int x)//算上x子树的贡献
{
for(re i=seg[x];i<=seg[x]+size[x]-1;i++)add(rev[i]);
}
il void del(int x)//单点减少
{
c[dep[x]].clear();
cnt[dep[x]]=0;
}
il void clear(int x)//清空x子树
{
for(re i=seg[x];i<=seg[x]+size[x]-1;i++)
del(rev[i]);
}
il void dfs1(int x,int fa)
{
dep[x]=dep[fa]+1;size[x]=1;seg[x]=++seg[0];rev[seg[x]]=x;
for(re i=head[x],y;i,y=go[i];i=next[i])
{
dfs1(y,x);size[x]+=size[y];
if(size[y]>size[son[x]])son[x]=y;
}
}
il void dfs2(int x,int flag)
{
for(re i=head[x],y;i,y=go[i];i=next[i])
{
if(y==son[x])continue;
dfs2(y,0);//先遍历轻儿子
}
if(son[x])dfs2(son[x],1);//再遍历重儿子
for(re i=head[x],y;i,y=go[i];i=next[i])
{if(y==son[x])continue;raise(y);}//更新轻儿子的贡献
add(x);//加上x结点本身的贡献
for(re i=0;i<q[x].size();i++)
ans[q[x][i].id]=cnt[dep[x]+q[x][i].k];//更新答案
if(!flag)clear(x);//如果是轻儿子,就清空
}
signed main()
{
n=read();
for(re i=1,x;i<=n;i++)cin>>s[i],x=read(),father[i]=x,Add(x,i);
for(re i=1;i<=n;i++)if(!father[i])dfs1(i,0);//预处理,倍增数组、dfs序等树上信息,记得要用循环,从每棵树的根节点出发
m=read();
for(re i=1,x,y;i<=m;i++)
{
x=read(),y=read();
if(dep[x]+y>=N)continue;//注意,如果询问的第K级儿子超过限制,不能存储,原因上面有
q[x].push_back((node){y,i});
}
for(re i=1;i<=n;i++)if(!father[i])dfs2(i,0);//找每棵树的根节点,0表示轻儿子,这样不用手动清空数组
for(re i=1;i<=m;i++)print(ans[i]),putchar('\n');
return 0;
}
\(\quad\)题目链接:CF1009F Dominant Indices(洛谷的链接)
\(\quad\) \(Dsu\) 板子题,用 \(cnt_i\) 数组来表示深度为i的结点数量,另外要注意要在修改的时候记录cnt最大的深度即可。
#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
#include<map>
#include<vector>
using namespace std;
#define re register int
#define int long long
#define LL long long
#define il inline
#define next nee
#define inf 1e18
il int read()
{
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)&&ch!='-')ch=getchar();
if(ch=='-')f=-1,ch=getchar();
while(isdigit(ch))x=(x<<1)+(x<<3)+ch-'0',ch=getchar();
return x*f;
}
il void print(int x)
{
if(x<0)putchar('-'),x=-x;
if(x/10)print(x/10);
putchar(x%10+'0');
}
const int N=1e6+5;
int n,ans[N],next[N<<1],go[N<<1],head[N],tot,seg[N],son[N],father[N],size[N];
int cnt[N],dep[N],rev[N],Maxdep,Max,num;
il void Add(int x,int y)
{
next[++tot]=head[x];
head[x]=tot;go[tot]=y;
}
il void check(int x)//更新Max和num的值
{
if(cnt[x]>Max)Max=cnt[x],num=x;
else if(cnt[x]==Max&&x<num)num=x;//如果有相等的情况,取深度小的
}
il void dfs1(int x,int fa)//预处理
{
father[x]=fa;size[x]=1;seg[x]=++seg[0];dep[x]=dep[fa]+1;rev[seg[x]]=x;//处理子树大小,父亲,深度,dfs序
if(dep[x]>Maxdep)Maxdep=dep[x];
for(re i=head[x],y;i,y=go[i];i=next[i])
{
if(y==fa)continue;dfs1(y,x);
size[x]+=size[y];
if(size[y]>size[son[x]])son[x]=y;//记录重儿子
}
}
il void dfs2(int x,int flag)
{
for(re i=head[x],y;i,y=go[i];i=next[i])
{
if(y==father[x]||y==son[x])continue;
dfs2(y,0);Max=num=0;//先遍历轻儿子
for(re j=seg[y];j<=seg[y]+size[y]-1;j++)
{int z=rev[j];cnt[dep[z]]=0;}//顺便清空cnt数组,Max和num清零
}if(son[x])dfs2(son[x],1);//再遍历重儿子
for(re i=head[x],y;i,y=go[i];i=next[i])
{
if(y==father[x]||y==son[x])continue;
for(re j=seg[y];j<=seg[y]+size[y]-1;j++)
{
int z=rev[j];cnt[dep[z]]++;//更新轻儿子的贡献
check(dep[z]);
}
}cnt[dep[x]]++;check(dep[x]);//加上x结点本身的贡献
ans[x]=num-dep[x];//更新答案
}
signed main()
{
n=read();
for(re i=1,x,y;i<n;i++)x=read(),y=read(),Add(x,y),Add(y,x);
dfs1(1,0);dfs2(1,1);
for(re i=1;i<=n;i++)print(ans[i]),putchar('\n');
return 0;
}
\(\quad\)题目链接:CF375D Tree and Queries(洛谷的链接)
思路
\(\quad\)标准做法是动态规划,但看到 \(4.5s\) 的时限,似乎可以树上启发式合并水过去,只要用 \(num_i\) 和 \(cnt_k\) 的数组来记录出现颜色 \(i\) 的数量及超过 \(k\) 的颜色数量即可。
#include<iostream>
#include<cstdio>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<map>
#include<vector>
using namespace std;
#define re register int
#define int long long
#define LL long long
#define il inline
#define next nee
#define inf 1e18
il int read()
{
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)&&ch!='-')ch=getchar();
if(ch=='-')f=-1,ch=getchar();
while(isdigit(ch))x=(x<<1)+(x<<3)+ch-'0',ch=getchar();
return x*f;
}
il void print(int x)
{
if(x<0)putchar('-'),x=-x;
if(x/10)print(x/10);
putchar(x%10+'0');
}
const int N=1e5+5;
int n,m,next[N<<1],go[N<<1],head[N],tot,seg[N],col[N];
int rev[N],size[N],son[N],father[N],cnt[N],ans[N],num[N];
struct node{int k,id;};
vector<node>q[N];
il void Add(int x,int y)
{
next[++tot]=head[x];
head[x]=tot;go[tot]=y;
}
il void add(int x){num[col[x]]++;cnt[num[col[x]]]++;}
il void raise(int x)
{
for(re i=seg[x];i<=seg[x]+size[x]-1;i++)
add(rev[i]);
}
il void clear(int x)
{
for(re i=seg[x];i<=seg[x]+size[x]-1;i++)
{
int y=rev[i];
cnt[num[col[y]]]--;num[col[y]]--;
}
}
il void dfs1(int x,int fa)
{
size[x]=1;seg[x]=++seg[0];rev[seg[x]]=x;father[x]=fa;
for(re i=head[x],y;i,y=go[i];i=next[i])
{
if(y==fa)continue;dfs1(y,x);
size[x]+=size[y];
if(size[y]>size[son[x]])son[x]=y;//重儿子
}
}
il void dfs2(int x,int flag)//flag表示是否为重儿子,1表示重儿子,0表示轻儿子
{
for(re i=head[x],y;i,y=go[i];i=next[i])
{
if(y==father[x]||y==son[x])continue;
dfs2(y,0);//先遍历轻儿子
}if(son[x])dfs2(son[x],1);//再遍历重儿子
for(re i=head[x],y;i,y=go[i];i=next[i])
{
if(y==father[x]||y==son[x])continue;
raise(y);//更新轻儿子的贡献
}add(x);//加上x结点本身的贡献
for(re i=0;i<q[x].size();i++)//更新答案
ans[q[x][i].id]=cnt[q[x][i].k];
if(!flag)clear(x);//如果是轻儿子,就清空
}
signed main()
{
n=read();m=read();
for(re i=1;i<=n;i++)col[i]=read();
for(re i=1,x,y;i<n;i++)x=read(),y=read(),Add(x,y),Add(y,x);
for(re i=1,x,y;i<=m;i++)x=read(),y=read(),q[x].push_back((node){y,i});
dfs1(1,0);dfs2(1,1);
for(re i=1;i<=m;i++)print(ans[i]),putchar('\n');
return 0;
}
\(\quad\)题目链接:CF741D Arpa’s letter-marked tree and Mehrdad’s Dokhtar-kosh paths(洛谷的链接)
\(\quad\)这其实算一道 Dsu 的压轴题,据说是树上启发式合并算法的创始者出的题。
\(\quad\)这题确实是有些难度的,总之我一开始连题解都没有看懂。
\(\quad\)首先考虑回文的问题,其他题解其实讲的很清楚了,只要22个字母中最多有一个字母数量为奇数即可,也可都为偶数,所以一共23种情况,但考虑所有情况(只分奇偶)有 \(2^{22}\)中情况,可以用一个二进制数表示,用 \(cnt_i\) 表示二进制数为 \(i\) 的结点的最大深度,二进制数指的是从这个结点到根节点的最短路径的序列,\(num_x\) 表示结点 \(x\) 到根节点的最短路径的序列,请仔细理解这句话,否则之后的代码可能会看不懂。
\(\quad\)然后我们对于两个修改函数都讲一遍。
\(\quad\)第一个修改函数,就是判断是否有有符合条件的,如对于节点 \(x\) 来说,和TA到根节点的序列为 \(num_x\),\(cnt_{num_x}\) 表示之前出现的另一条大小为 \(num_x\) 序列,这样这两条路径合并后字母数就都是偶数,之后的循环枚举的是有一个字母不同的情况,这两种情况都是符合条件的。
il void add1(int x)
{
ans[now]=max(ans[now],dep[x]+cnt[num[x]]);
for(re i=0;i<=21;i++)ans[now]=max(ans[now],dep[x]+cnt[(1<<i)^num[x]]);
}
\(\quad\)对于第二个修改函数,就是把这个结点 \(x\) 的信息载入 \(cnt\) 数组,并且为了最后的序列最长,要尽可能选深度大的,显然深度大的答案更优。
il void add2(int x)
{cnt[num[x]]=max(cnt[num[x]],dep[x]);}
\(\quad\)注意要先做修改操作 \(1\),再做修改操作 \(2\),也就是说先统计这个点的答案(或一棵子树),再载入这个点的数据(或一棵子树),否则答案会把自己也记进去,可以仔细思考一下这个点。
\(\quad\)接下来我们思考一个问题,因为我们是一棵子树一棵子树为单位修改的,如果这个最优答案在子树中会怎么样?可以发现这样的答案在子树中一定被统计过了,当这条路径的两个端点的LCA被询问时就以及被记录了,所以还要跑一遍所有子树,用子树的答案来更新当前结点。
\(\quad\)另外我们还要注意节点 \(i\) 的答案的计算公式为
\(\quad\)这其实就是 \(x\),\(y\) 两点之间的距离公式( \(x\),\(y\) 为最短路径的两个端点),另外可以发现最优情况下结点 \(i\) 为结点 \(x\) 和结点 \(y\) 的LCA,因为结点 \(x\) 和结点 \(y\) 的在以 \(i\) 为根节点的子树,若不是的话,那么答案就会算多,但这显然是错误的答案,所以我们是一棵子树一棵子树为单位修改的,这也算回答了上面的问题。
\(\quad\)最后来看看完整代码吧!
#include<iostream>
#include<cstdio>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<map>
#include<vector>
using namespace std;
#define re register int
#define il inline
#define next nee
#define inf 1e9+5
il int read()
{
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)&&ch!='-')ch=getchar();
if(ch=='-')f=-1,ch=getchar();
while(isdigit(ch))x=(x<<1)+(x<<3)+ch-'0',ch=getchar();
return x*f;
}
il void print(int x)
{
if(x<0)putchar('-'),x=-x;
if(x/10)print(x/10);
putchar(x%10+'0');
}
const int N=5e5+5;
int n,m,next[N],go[N],head[N],tot,seg[N],son[N],father[N],now;
int size[N],rev[N],ans[N],s[N],dep[N],num[N],cnt[1<<23];
il int Max(int x,int y){return x>y?x:y;}
il void Add(int x,int y,int z)
{
next[++tot]=head[x];
head[x]=tot;go[tot]=y;s[tot]=z;
}
il void add1(int x)//修改操作1
{
ans[now]=max(ans[now],dep[x]+cnt[num[x]]);
for(re i=0;i<=21;i++)ans[now]=max(ans[now],dep[x]+cnt[(1<<i)^num[x]]);
}
il void add2(int x)//修改操作2
{cnt[num[x]]=max(cnt[num[x]],dep[x]);}
il void clear(int x)//清空操作
{
for(re i=seg[x];i<=seg[x]+size[x]-1;i++)
cnt[num[rev[i]]]=-inf;
}
il void dfs1(int x)
{
dep[x]=dep[father[x]]+1;size[x]=1;seg[x]=++seg[0];rev[seg[x]]=x;
for(re i=head[x],y;i,y=go[i];i=next[i])
{
num[y]=num[x]^(1<<s[i]);dfs1(y);
size[x]+=size[y];
if(size[y]>size[son[x]])son[x]=y;
}
}
il void dfs2(int x,int flag)
{
for(re i=head[x],y;i,y=go[i];i=next[i])
{
if(y==son[x])continue;
dfs2(y,0);
}if(son[x])dfs2(son[x],1);now=x;
for(re i=head[x],y;i,y=go[i];i=next[i])
{
if(y==son[x])continue;
for(re i=seg[y];i<=seg[y]+size[y]-1;i++)add1(rev[i]);
for(re i=seg[y];i<=seg[y]+size[y]-1;i++)add2(rev[i]);
}add1(x),add2(x);//记得要修改x结点
ans[x]-=(dep[x]<<1);//减去本身的深度
for(re i=head[x],y;i,y=go[i];i=next[i])ans[x]=max(ans[x],ans[y]);
if(!flag)clear(x);
}
signed main()
{
n=read();char ch;
for(re i=0;i<(1<<22);i++)cnt[i]=-inf;//一定要初始化为负值
for(re i=2,x;i<=n;i++){x=read();father[i]=x;scanf("%c",&ch);Add(x,i,ch-'a');}
dfs1(1);dfs2(1,1);
for(re i=1;i<=n;i++)print(Max(ans[i],0)),putchar(' ');//可能会输出负数
return 0;
}
另外好像还有两道题:P3224 永无乡,UVA1479 Graph and Queries,据说要用到平衡树,蒟蒻现在还没有学,以后有时间再写吧!
还有这个黑题:CF715C Digit Tree
\(\quad\)讲这么多,有点累了,这 \(7\) 题写了快两天,都快调吐了。
\(\quad\)参考资料: