[CF741D] Arpa’s letter-marked tree and Mehrdad’s Dokhtar-kosh paths [树链剖分/树上启发式合并]

本文介绍了一种利用树剖分和启发式合并算法求解有根树中每个子树的最长回文串路径问题的方法。通过使用二进制数表示字母出现次数的奇偶性,并结合树链剖分和动态维护最大深度的技术,实现了复杂度为Θ(NlogN)的高效算法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

题意:给定一棵NNN点有根树,每条树边上有aaavvv里的某个字母。
求每个子树中最长,满足路径上的字母重排之后可以构成回文串的路径。
N≤5∗105N \le 5*10^5N5105

CF741是单片高性能内补偿运算放大器

显然回文串里面最多只有一个字母出现奇数次,可以用222222位的二进制数来表示每个字母出现次数的奇偶情况。
val(i)val(i)val(i)为从总根111iii路径上每个字母出现次数的奇偶情况(压进二进制数里)。
考虑现在正在求以xxx为根的子树的答案AnsAnsAns
Ans=max(dep(u)+dep(v)−2∗dep(lca(u,v)),val(u)∧val(v)=2p,p∈[1,22]∩Z∗  or  0,dep(lca(u,v))≤dep(x)Ans=max(dep(u)+dep(v)-2*dep(lca(u,v)),val(u)\land val(v)=2^{p,p\in[1,22]\cap Z^*}\;or\;0,dep(lca(u,v))\le dep(x)Ans=max(dep(u)+dep(v)2dep(lca(u,v)),val(u)val(v)=2p,p[1,22]Zor0,dep(lca(u,v))dep(x)

数据范围明示复杂度Θ(NlogN)\Theta(NlogN)Θ(NlogN)

很明显这不是一个点分治题目,不过这个既视感还是挺重的
考虑把路径分为过rootrootroot和不过rootrootroot
不经过rootrootroot的交给下面的lca(u,v)lca(u,v)lca(u,v)去搞,过rootrootroot的就地解决

rootrootroot的也可以分成两种(虽然不分也行),一种是有一个端点是根的,
另外那种,枚举到某个点xxx的时候找出满足val(x)∧val(y)=2p,p∈[1,22]∩Z∗  or  0val(x) \land val(y)=2^{p,p\in[1,22]\cap Z^*}\;or\;0val(x)val(y)=2p,p[1,22]Zor0dep(y)dep(y)dep(y)最大的yyy
只需要枚举2p,p∈[1,22]∩Z∗  or  02^{p,p\in[1,22]\cap Z^*}\;or\;02p,p[1,22]Zor0一共232323种异或结果然后拿val(x)val(x)val(x)去对出val(y)val(y)val(y)就行了
对于每个valvalval记录一个max_depmax\_depmax_dep
那么可以树剖暴力或者搞树上启发式合并
Θ(23∗NlogN)\Theta(23*NlogN)Θ(23NlogN)

#include<cstdio>
#include<iostream>
#include<cstdlib>
#include<algorithm>
#include<cstring>
#include<cmath>
#include<cctype>
using namespace std;
#define add_edge(u,v,w) nxt[++tot]=head[u],to[tot]=v,head[u]=tot,val[tot]=1<<w
int N,t,tot=0,mx=0,mn;
int head[500005]={},nxt[500005]={},to[500005]={},val[500005]={}; //链式前向星 
int dep[500005]={},sum[500005]={},mxdep[5000005]={}; //深度,异或和,异或和最大深度 
int siz[500005]={},son[500005]={},ans[500005]={}; //子树大小,重儿子,答案 
char ch;
void pthDec(int x) //树链剖分 
{
    siz[x]=1; //初始化子树大小为1
    for(int i=head[x];i;i=nxt[i])
    {
        dep[to[i]]=dep[x]+1; sum[to[i]]=sum[x]^val[i]; //更新子节点的深度,到根路径的权值异或和 
        pthDec(to[i]); //递归 
        siz[x]+=siz[to[i]]; //更新子树大小
        if(siz[to[i]]>siz[son[x]])son[x]=to[i]; //维护重儿子 
    }
}
void calc(int x,int root) //计算答案 
{
    mx=max(mx,mxdep[sum[x]]+dep[x]-2*dep[root]); //不到根,xorsum=0
    for(int i=0;i<22;++i)mx=max(mx,mxdep[(1<<i)^sum[x]]+dep[x]-2*dep[root]); //不到根,xorsum=2^p 
    //这两句决定了mxdep要初始化为-inf
    
    if(!(sum[x]^sum[root]))mx=max(mx,dep[x]-dep[root]); //到根,xorsum=0 
    for(int i=0;i<22;++i)if((sum[x]^sum[root])==(1<<i))mx=max(mx,dep[x]-dep[root]); //到根,xorsum=2^p 
    
    for(int i=head[x];i;i=nxt[i])calc(to[i],root); //递归 
}
void modify(int x,bool type) //统计或者抹除贡献 
{
    mxdep[sum[x]]=type?max(mxdep[sum[x]],dep[x]):mn; //更新mxdep 
    for(int i=head[x];i;i=nxt[i])modify(to[i],type); //递归 
}
void dfs(int x,bool keep)
{
    for(int i=head[x];i;i=nxt[i])
    {
        if(to[i]==son[x])continue;
        dfs(to[i],0); //递归轻儿子 
    }
    if(son[x])dfs(son[x],1); //递归重儿子 
    
    mx=max(0,mxdep[sum[x]]-dep[x]); //重儿子,xorsum=0 
    for(int i=0;i<22;++i)mx=max(mx,mxdep[(1<<i)^sum[x]]-dep[x]); //重儿子,xorsum=2^p
    
    for(int i=head[x];i;i=nxt[i])
    {
    	if(to[i]==son[x])continue;
    	calc(to[i],x); modify(to[i],1); //暴力统计轻儿子贡献 
    }
    ans[x]=mx; //保存答案 
    
    if(keep)mxdep[sum[x]]=max(mxdep[sum[x]],dep[x]); //保存贡献 
    else //抹除贡献 
    {
    	for(int i=head[x];i;i=nxt[i])modify(to[i],0); //子节点的 
    	mxdep[sum[x]]=0; //根的 
    }
}
void sumans(int x) //树形dp合并答案 
{
    for(int i=head[x];i;i=nxt[i])
    {
        sumans(to[i]); //递归 
        ans[x]=max(ans[x],ans[to[i]]); //合并 
    }
}
int main()
{
    memset(mxdep,128,sizeof(mxdep)); mn=mxdep[0]; //初始化为极小值-inf
    
    scanf("%d",&N);
    for(int i=2;i<=N;++i)
    {
        scanf("%d %c ",&t,&ch);
        add_edge(t,i,(int)(ch-'a'));
    } //读入,连边 
    pthDec(1); //树剖 
    dfs(1,0);  //暴力统计每个点独立的答案(过每个点的路径) 
    sumans(1); //合并统计答案(把子树的答案合并到根) 
    for(int i=1;i<=N;++i)printf("%d ",ans[i]); //输出 
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值