洛谷P6623——[省选联考 2020 A 卷] 树

传送门:QAQQAQ

 

题意:自己看

 

思路:正解应该是线段树/trie树合并? 但是本蒟蒻啥也不会,就用了树上二次差分

(思路来源于https://www.luogu.com.cn/blog/dengyaotriangle/solution-p6623)


首先我们企图树形DP,但是发现每一个元素往上推一格都会+1,所以我们对于二进制每一位考虑贡献。

顶点u对他祖先的二进制第k位贡献,可能是0可能是1,但不断+1时变化是一个混循环,刨掉最开始的,后面都是规则的循环,2^k个0,2^k个1。所以我们可以对有影响的1进行第一次差分

但是因为有多个区间,尤其是$k=0$时复杂度到达O(n2),所以我们再用一次差分:考虑到差分每次赋值的上下两个点分别对于2^k同余,所以我们对于余数再建一个数组,改的区间一定是连续的,就又可以优化成O(1)了

总复杂度$O(nlog(n))$

 

代码:

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=530000;
int a[N],f[N],n,val[N],dp[21][N],dep[N];//(dep+a[])%2^k==i
int base[21];
ll ans=0;
vector<int> v[N];

int dfs(int u)
{
    int ret=a[u];
    for(int i=0;i<=20;i++) dp[i][(dep[u]+a[u])%base[i]]^=base[i];
    for(int i=0;i<=20;i++) ret^=dp[i][dep[u]%base[i]]; 
    for(int i=0;i<v[u].size();i++)
    {
        int to=v[u][i];
        dep[to]=dep[u]+1;
        ret^=dfs(to);
    }
    for(int i=0;i<=20;i++) ret^=dp[i][dep[u]%base[i]]; 
    //上下两个相同的操作是为了异或掉不是u子树的贡献 
    ans+=1LL*ret;
    return ret;
}

int main()
{
    base[0]=1;
    for(int i=1;i<=20;i++) base[i]=base[i-1]*2;
    scanf("%d",&n);
    for(int i=1;i<=n;i++) scanf("%d",&a[i]);
    for(int i=2;i<=n;i++)
    {
        scanf("%d",&f[i]);
        v[f[i]].push_back(i);
    }
    dfs(1);
    cout<<ans<<endl;
    return 0;
}

这份代码让刚才那个思路更好实现:假如u下面的子树已经算好,那么所有元素+1后再去考虑二进制每一位的贡献,当且仅当该位0->1或1->0时才要异或,即在循环节中间或末尾,即$a[v]+dep[v]$与$dep[u]$关于2^k同余(2^(k+1)为循环节)

 

(留坑更新线段树合并做法)

posted @ 2020-06-30 23:22  'Clovers'  阅读(190)  评论(2编辑  收藏  举报