题目描述
一棵根为1 的树,每条边上有一个字符(a-v共22种)。 一条简单路径被称为Dokhtar-kosh当且仅当路径上的字符经过重新排序后可以变成一个回文串。 求每个子树中最长的Dokhtar-kosh路径的长度。
给你n个点构成的一棵树,树里面的每一条边有一个权值,求出每个子树里面能通过重排构成回文串的最大路径长度
Input
4
1 s
2 a
3 sOutput
3 1 1 0
题解
比较好的一道dsu on tree的题,而且还是发明者专门为这个算法出的一道题。
首先我们需要从一个点来入手,容易发现字符只有22种,比较少,而满足重排后能构成回文串的条件是:每个字符出现次数都为偶数或出现次数为奇数的字符仅有一个。
因此我们可以考虑将其压缩成0表示出现次数为偶数,1表示出现次数为奇数。
而对于一个路径上的字符,我们只需考虑每个字符的出现次数,这样我们巧妙地把问题状压了一下,用一个整数就可以表达出所有符合条件的状态:0 或者 (1<<i),0<=i<=21,总共22种。
之后我们可以考虑维护一个值 dis[i] 表示 从 i 到 1 的路径上字符的状态,其实利用异或的性质,dis[i]就是从 1 到 i 路径里面所有字符表示的状态异或的答案。
同时我们如果知道两个点,怎样求出两个点形成的状态呢,也是利用异或的性质,假设有两个点u和v,dis[u]^dis[v]^dis[lca(u,v)]^dis[lca(u,v)]其实就是答案,简化一下就是 dis[u]^dis[v],因为两个点的lca上的部分会因为异或的性质抵消掉。
那么我们就很容易发现如果两个节点之间形成的路径符合条件的话,那么dis[u]^dis[v] = 0 | (1<<i),1<=i<=21
这里我们还需要在求每个子树的答案维护一个值,即f[dis[i]] 表示在以当前节点为根的子树中状态为dis[i] 的最大深度。
然后我们先按照dsu on tree 套路,先去遍历每一个轻儿子,计算数据但是不保留,之后处理重儿子,计算数据并且保留,最后再计算轻儿子和当前节点对答案的贡献。
具体过程参考代码,时间复杂度O(23nlogn)
代码实现
#pragma GCC optimize(2) #include<iostream> #include<algorithm> #include<cmath> #include<cstring> #include<cstdio> #include<cstdlib> #include<vector> #include<map> #include<set> #include<stack> #include<queue> #define PI atan(1.0)*4 #define E 2.718281828 #define rp(i,s,t) for (register int i = (s); i <= (t); i++) #define RP(i,s,t) for (register int i = (t); i >= (s); i--) #define ll long long #define ull unsigned long long #define mst(a,b) memset(a,b,sizeof(a)) #define lson l,m,rt<<1 #define rson m+1,r,rt<<1|1 #define pii pair<int,int> #define mp make_pair #define pb push_back using namespace std; inline int read() { int a=0,b=1; char c=getchar(); while(c<'0'||c>'9') { if(c=='-') b=-1; c=getchar(); } while(c>='0'&&c<='9') { a=(a<<3)+(a<<1)+c-'0'; c=getchar(); } return a*b; } const int INF = 0x3f3f3f3f; const int N = 5e5+7; int tot,L[N],R[N],Id[N];//用来维护dfs序 int head[N],cnt; struct Edge{ int to,nxt,w; }e[N];//链式向前星存储树 int deep[N],sz[N],hSon[N];//分别记录节点的深度,大小,以及重儿子 int ans[N],visited[N];//记录每个点的答案和标记是否访问过 int f[N*10],dis[N]; //dis[i]表示从1(根节点)到 i 点路径上的状态 //f[i]表示在以当前节点为根的子树中状态为 i 的最大深度 void addEdge(int u,int v,int w){//建边 e[++cnt]=(Edge){v,head[u],w}; head[u]=cnt; } void dfs1(int u,int f){//这一次dfs是预先求出每个节点的深度,重儿子,以及每个节点的dfs序,常规操作 deep[u]=deep[f]+1; sz[u]=1; L[u]=++tot; Id[tot]=u; for(int i=head[u];i;i=e[i].nxt){ int v=e[i].to; dis[v]=dis[u]^e[i].w; dfs1(v,u); sz[u]+=sz[v]; if(sz[hSon[u]]<sz[v]) hSon[u]=v; } R[u]=tot; } void calc(int u){ //这里f[dis[u]]表示在以u为根的子树中状态为dis[u]的最大深度 if(f[dis[u]]) ans[u]=max(ans[u],f[dis[u]]-deep[u]); rp(i,0,21) if(f[dis[u]^(1<<i)]) ans[u]=max(ans[u],f[dis[u]^(1<<i)]-deep[u]); //这里看着有点不好理解,其实就是利用了异或的性质,我们首先需要考虑以u为起点对答案的贡献 //因为dis[u]表示从1到u的状态,所以如果符合条件一定满足 x^dis[u]=0||(1<<i) -> x = dis[u] ^ 0||(1<<i); f[dis[u]]=max(f[dis[u]],deep[u]);//更新和维护f数组 for(int i=head[u];i;i=e[i].nxt){ int v=e[i].to; if(v==hSon[u]||visited[v]) continue; rp(j,L[v],R[v]){//这里考虑的是以u为中间点的情况,其实和上面情况类似 int x=Id[j]; //其实就是看另一边是否出现相同满足条件状态,出现的话更新答案 if(f[dis[x]]) ans[u]=max(ans[u],f[dis[x]]+deep[x]-2*deep[u]); rp(k,0,21) if(f[dis[x]^(1<<k)]) ans[u]=max(ans[u],f[dis[x]^(1<<k)]+deep[x]-2*deep[u]); } rp(j,L[v],R[v]) f[dis[Id[j]]]=max(f[dis[Id[j]]],deep[Id[j]]);//维护f数组 } } void dfs2(int u,int keep){//keep为 1 表示处理并保留数据,否则表示处理但不保留数据 for(int i=head[u];i;i=e[i].nxt){//先对轻儿子进行处理答案,但是不保留数据 int v=e[i].to; if(v==hSon[u]) continue; dfs2(v,0); ans[u]=max(ans[u],ans[v]);//更新答案,因为有可能最大长度的链不以当前节点为根节点 } if(hSon[u]) dfs2(hSon[u],1),ans[u]=max(ans[u],ans[hSon[u]]),visited[hSon[u]]=1;//对重儿子进行类似处理,但是保留数据并且标记 calc(u);//更新答案 if(hSon[u]) visited[hSon[u]]=0; if(keep==0) rp(i,L[u],R[u]) f[dis[Id[i]]]=0;//如果为轻儿子,则清空数据 } int main(){ int n=read(); rp(i,2,n){ int x;char s[10]; scanf("%d%s",&x,s); addEdge(x,i,1ll<<(s[0]-'a')); } dfs1(1,1); dfs2(1,1); rp(i,1,n) printf("%d ",ans[i]); return 0; }
CF741D——树上启发式合并+状压优化
最新推荐文章于 2023-04-06 21:04:31 发布