Liveddd's Blog

愛にできることはまだあるかい

P6623 [省选联考 2020 A 卷] 树

有比较好想的 Trie 树做法。但是树上差分更巧妙。

从下往上做。从儿子继承到父亲,使得儿子内所有点到父亲的距离等于儿子内所有点到儿子的距离 $+1$,也就是全局 $+1$ 操作。并且维护的信息是异或和。类似于线段树合并,我们直接 01-Trie 合并即可。时间复杂度 $\mathcal O(n\log V)$。

换种思路,我们对于每一个 $v_i$ 分别考虑其对祖先的贡献。考虑整个 $+1$ 然后进位的过程,对于第 $k$ 位为 $1$,那么对它的祖先的第 $k$ 位贡献 $\underbrace{11\cdots 11}_{2^k-(v_i\text{ mod } 2^k)}\underbrace{00\cdots 00}_{2^k}\underbrace{11\cdots 11}_{2^k}$。根据这一点进行树上差分。给定差分数组 $g[x][i]$ 表示 $x$ 第 $i$ 位的差分。把当前 $v_i$ 每一位的差分往上传递到,把 $g[x][i]$ 的差分到 $g[fa_{x,2^i}][i]$。需要长链剖分求 $k$ 级祖先,时间复杂度 $\mathcal O(n\log n + n\log V)$。常数要小一点。

实际上有更好的实现方式。我们发现对于每个点,产生差分的点的充要条件是 $d_i=x\pmod {2^k}$。我们开一个新的 $p[k][x]$ 表示 $d_i=x\pmod {2^k}$ 的差分。递归之前先去掉已经原来的差分数组,再递归得到子树的贡献,再算出当前点的答案即可。时间复杂度 $\mathcal O(n\log V)$。常数很小。

具体实现参考如下。

Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include<iostream>
#include<cstdio>
#define ll long long
using namespace std;
const int N=5.26e5+10,K=21;
template <class T>
inline void read(T &x)
{
x=0;bool f=0;
char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')f=1;ch=getchar();}
while(ch>='0'&&ch<='9')x=(x<<1)+(x<<3)+(ch^48),ch=getchar();
if(f)x=-x;
}
template <class T,class ...T1>
inline void read(T &x,T1 &...x1)
{
read(x),read(x1...);
}
int n;
int a[N],p[K][N];
int tot,head[N],ver[N<<1],ne[N<<1];
ll ans;
inline void add(int u,int v)
{
ver[++tot]=v;
ne[tot]=head[u];
head[u]=tot;
}
int dfs(int x,int d)
{
int res=a[x];
for(int i=0;i<K;i++)res^=p[i][d&((1<<i)-1)];
for(int i=head[x];i;i=ne[i])
{
int y=ver[i];
res^=dfs(y,d+1);
}
for(int i=0;i<K;i++)res^=p[i][d&((1<<i)-1)];
for(int i=0;i<K;i++)p[i][(d+a[x])&((1<<i)-1)]^=1<<i;
ans+=res;
return res;
}

int main()
{
read(n);
for(int i=1;i<=n;i++)read(a[i]);
for(int i=2,fa;i<=n;i++)read(fa),add(fa,i);
dfs(1,0);
printf("%lld\n",ans);
return 0;
}