题目
题目链接:https://www.luogu.com.cn/problem/P6623
给定一棵 \(n\) 个结点的有根树 \(T\),结点从 \(1\) 开始编号,根结点为 \(1\) 号结点,每个结点有一个正整数权值 \(v_i\)。
设 \(x\) 号结点的子树内(包含 \(x\) 自身)的所有结点编号为 \(c_1,c_2,\dots,c_k\),定义 \(x\) 的价值为:
\[val(x)=(v_{c_1}+d(c_1,x)) \oplus (v_{c_2}+d(c_2,x)) \oplus \cdots \oplus (v_{c_k}+d(c_k, x))
\]
其中 \(d(x,y)\) 表示树上 \(x\) 号结点与 \(y\) 号结点间唯一简单路径所包含的边数,\(d(x,x) = 0\)。\(\oplus\) 表示异或运算。
请你求出 \(\sum\limits_{i=1}^n val(i)\) 的结果。
思路
考虑从答案从 \(x\) 的子节点如何转移到 \(x\) 上来:显然是每一个节点的权值加一后再异或起来。
把每一个节点的权值 + 到目前根的距离转成二进制,由低位到高位扔进一棵 Trie 中,那么把所有子树内的点权值加一,其实就是沿着 Trie 边权为 1 的点走下去,并且把沿路遇到的 0 给变成 1,再把沿路的 1 变为 0。那么其实就是将遍历到的节点的左右子树交换了。
插入自己本身的权值简单,那么我们就成功搞定加一操作和插入操作。我们只需要将 \(x\) 的各个子节点的 Trie 合并到 \(x\) 上。类似于线段树合并。
时间复杂度 \(O(n\log n)\)。
代码
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
typedef long long ll;
const int N=525020,LG=25;
int n,tot,a[N],head[N],rt[N];
ll ans;
struct edge
{
int next,to;
}e[N];
void add(int from,int to)
{
e[++tot].to=to;
e[tot].next=head[from];
head[from]=tot;
}
struct Trie
{
int tot,lc[N*LG],rc[N*LG],val[N*LG],size[N*LG];
void pushup(int x,int dep)
{
val[x]=val[lc[x]]^val[rc[x]];
if (size[rc[x]]&1) val[x]^=(1<<dep);
}
int merge(int x,int y,int dep)
{
if (!x || !y) return x+y;
size[x]+=size[y];
lc[x]=merge(lc[x],lc[y],dep+1);
rc[x]=merge(rc[x],rc[y],dep+1);
pushup(x,dep);
return x;
}
void update(int x,int dep)
{
if (!x) return;
swap(lc[x],rc[x]);
update(lc[x],dep+1);
pushup(x,dep);
}
int ins(int x,int val,int dep)
{
if (dep>22) return 0;
if (!x) x=++tot;
size[x]++;
if (val&(1<<dep)) rc[x]=ins(rc[x],val,dep+1);
else lc[x]=ins(lc[x],val,dep+1);
pushup(x,dep);
return x;
}
}trie;
void dfs(int x)
{
for (int i=head[x];~i;i=e[i].next)
{
dfs(e[i].to);
rt[x]=trie.merge(rt[x],rt[e[i].to],0);
}
trie.update(rt[x],0);
rt[x]=trie.ins(rt[x],a[x],0);
ans+=trie.val[rt[x]];
}
int main()
{
memset(head,-1,sizeof(head));
scanf("%d",&n);
for (int i=1;i<=n;i++)
scanf("%d",&a[i]);
for (int i=2,x;i<=n;i++)
{
scanf("%d",&x);
add(x,i);
}
dfs(1);
printf("%lld",ans);
return 0;
}