树上启发式合并

树上启发式合并,\(DSU\) \(On\) \(Tree\),静态链分治,用于求解支持离线的树上子树查询问题。

暴力做法,每次做完一棵树就要把它清空,避免对它兄弟造成影响,但是做到它的祖先时又会重新对它做一遍,发现最后一棵树是不需要清空的,于是考虑将节点数最多的留到最后。

首先对树进行轻重链剖分,找出重儿子,在计算答案时,先遍历\(x\)的轻儿子,但不保留其影响,之后遍历\(x\)的重儿子,保留影响,最后再次遍历\(x\)的轻儿子,得到\(x\)的答案。

除了重儿子,每次遍历完后要恢复之前的状态。

CF208E Blood Cousins

每次询问一个点x和k,问由多少个点和x由共同的k级祖先。

倍增预处理,将询问离线到x的k级祖先,用cnt数组记录当前子树内不同深度的出现次数。

inline int find(int x,int k){
    for(int i=18;~i;i--)if(k>=1<<i)k-=1<<i,x=fa[x][i];
    /* for(int i=18;~i;i--)if(k>=dep[x]-dep[fa[x][i]])k-=dep[x]-dep[fa[x][i]],x=fa[x][i];效果一样*/
    return x;
}
void dsu(int x,bool isson){
    for(int i=h[x];i;i=e[i].next){
        int y=e[i].to;
        if(y==son[x])continue;
        dsu(y,false);/*先遍历轻儿子*/
    }
    if(son[x])dsu(son[x],true);/*有重儿子则遍历重儿子*/
    for(int i=h[x];i;i=e[i].next){
        int y=e[i].to;
        if(y==son[x])continue;
        for(int j=ipt[y];j<=ipt[y]+sz[y]-1;j++)cnt[dep[mp[j]]]++;/*dfs序映射子树内节点,深度更新*/
    }
    cnt[dep[x]]++;/*更新x自己的深度*/
    for(auto a:v[x])ans[a.id]=cnt[dep[x]+a.k]-1;/*询问点的k级祖先为x,即x的k级后代为询问点,查询相同深度的点的个数并减去询问自己*/
    if(isson==false)for(int i=ipt[x];i<=ipt[x]+sz[x]-1;i++)cnt[dep[mp[i]]]--;/*复原*/
}

CF600E Lomsat gelral

每个点有颜色,求每棵子树占据主导地位的颜色的编号和。

为每个颜色开一个桶,记录当前最大值和\(sum\),每次由更新操作时比较\(cnt\)和最大值,相等则累加\(sum\),cnt大于最大值时更新最大值并重置\(sum\),清除轻儿子贡献时清空最大值和\(sum\).

void dsu(int x,bool isson){
    for(int i=h[x];i;i=e[i].next){
        int y=e[i].to;
        if(y==son[x]||y==fa[x])continue;
        dsu(y,false);
    }
    if(son[x])dsu(son[x],true);
    cnt[col[x]]++;
    if(cnt[col[x]]>ma)ma=cnt[col[x]],sum=col[x];
    else if(cnt[col[x]]==ma)sum+=col[x];
    for(int i=h[x];i;i=e[i].next){
        int y=e[i].to;
        if(y==son[x]||y==fa[x])continue;
        for(int j=ipt[y];j<=ipt[y]+sz[y]-1;j++){
            cnt[col[mp[j]]]++;
            if(cnt[col[mp[j]]]>ma)ma=cnt[col[mp[j]]],sum=col[mp[j]];
            else if(cnt[col[mp[j]]]==ma)sum+=col[mp[j]];
        }
    }
    ans[x]=sum;
    if(isson==false){
        sum=ma=0;
        for(int i=ipt[x];i<=ipt[x]+sz[x]-1;i++)cnt[col[mp[i]]]--;
    }
}

CF1709E XOR Tree

每个点有点权,定义一棵树合法当且仅当不存在简单路径的异或和为0,每次可以改变一个点权成为任意数,求最少操作数使得树合法。

假设将\(x\)的点权修改为\(2^{30+x}\),那么所有经过\(x\)的路径的异或和都不会为\(0\)。为每一个点开一个集合存子树内所有点的\(dis\),即\(1\)到该点的异或和,枚举\(x\)的儿子\(y\),将\(y\)的集合合并到\(x\)上面,强制\(x\)\(size\)大于\(y\)\(size\),枚举\(y\)中每一个元素\(k\),若在\(x\)中可以找到\(a[x]\) \(xor\) \(k\),那么\(x\)需要修改,之后将\(x\)的集合清空。

void dfs(int x,int fa){
    mp[x][dis[x]]=true;
    bool flag=false;
    for(auto y:v[x]){
        if(y==fa)continue;
        dis[y]=dis[x]^a[y];
        dfs(y,x);
        if(mp[x].size()<mp[y].size())swap(mp[x],mp[y]);
        for(auto k:mp[y])if(mp[x].find(a[x]^k.first)!=mp[x].end()){
            flag=true;
            break;
        }
        for(auto k:mp[y])mp[x][k.first]=true;
    }
    if(flag==true)mp[x].clear(),ans++;
}

CF246E Blood Cousins Return

一片森林,每个点有一个字符串作为名字,每次询问点\(x\)\(k\)级子孙共有多少个不同的名字。

\(unordered\_map\)维护同一个深度的字符串集合,撤销轻儿子操作时则清空它。

void dsu(int x,bool isson){
    for(int i=h[x];i;i=e[i].next){
        int y=e[i].to;
        if(y==son[x])continue;
        dsu(y,false);
    }
    if(son[x])dsu(son[x],true);
    for(int i=h[x];i;i=e[i].next){
        int y=e[i].to;
        if(y==son[x])continue;
        for(int j=ipt[y];j<=ipt[y]+sz[y]-1;j++)cnt[dep[mp[j]]][s[mp[j]]]=true;
    }
    cnt[dep[x]][s[x]]=true;
    for(auto a:v[x])ans[a.id]=cnt[a.x].size();
    if(isson==true)return;
    for(int i=ipt[x];i<=ipt[x]+sz[x]-1;i++)cnt[dep[mp[i]]].clear();
}
    for(int i=1;i<=m;i++){
        int x,k;
        cin>>x>>k;
        v[x].push_back({i,dep[x]+k});
    }

CF375D Tree and Queries

询问一个点的子树内出现次数 \(>=k\) 的颜色种类。

\(cnt[x]\) 表示颜色 \(x\) 的出现次数,\(sum[x]\) 表示出现次数 \(>=x\) 的颜色种类数,在添加颜色时,两个桶都是增加的,\(sum[x]\) 相当于一个前缀和。

void dsu(int x,bool isson){
    for(auto y:v[x]){
        if(y==fa[x]||y==son[x])continue;
        dsu(y,false);
    }
    if(son[x])dsu(son[x],true);
    for(auto y:v[x]){
        if(y==fa[x]||y==son[x])continue;
        for(int i=ipt[y];i<=ipt[y]+sz[y]-1;i++)sum[++cnt[col[mp[i]]]]++;
    }
    sum[++cnt[col[x]]]++;
    for(auto a:q[x])ans[a.id]=sum[a.k];
    if(isson==true)return;
    for(int i=ipt[x];i<=ipt[x]+sz[x]-1;i++)--sum[cnt[col[mp[i]]]--];
}

CF570D Tree Requests

询问一个点的子树内给定深度的点重排后能否组成回文串。

每一个深度开一个桶进行维护,奇数个数 \(<=1\) 则可以。

void dsu(int x,bool isson){
    for(auto y:v[x])if(y^son[x])dsu(y,false);
    if(son[x])dsu(son[x],true);
    for(auto y:v[x]){
        if(y==son[x])continue;
        for(int i=ipt[y];i<=ipt[y]+sz[y]-1;i++)cnt[dep[mp[i]]][s[mp[i]]-'a']++;
    }
    cnt[dep[x]][s[x]-'a']++;
    for(auto a:q[x]){
        int num=0;
        for(int i=0;i<26;i++)num+=cnt[a.x][i]&1;
        ans[a.id]=num<=1;
    }
    if(isson==true)return;
    for(int i=ipt[x];i<=ipt[x]+sz[x]-1;i++)cnt[dep[mp[i]]][s[mp[i]]-'a']--;
}

也可以 \(O(1)\) 判断,利用二进制,若 \(x==(x\&-x)\) 则说明 \(x\) 全为 \(0\)\(x\) 的二进制下只有一位 \(1\).

void dsu(int x,bool isson){
    for(auto y:v[x])if(y^son[x])dsu(y,false);
    if(son[x])dsu(son[x],true);
    for(auto y:v[x]){
        if(y==son[x])continue;
        for(int i=ipt[y];i<=ipt[y]+sz[y]-1;i++)check[dep[mp[i]]]^=1<<s[mp[i]]-'a';
    }
    check[dep[x]]^=1<<s[x]-'a';
    for(auto a:q[x])ans[a.id]=check[a.x]==(check[a.x]&-check[a.x]);
    if(isson==true)return;
    for(int i=ipt[x];i<=ipt[x]+sz[x]-1;i++)check[dep[mp[i]]]^=1<<s[mp[i]]-'a';
}

CF1009F Dominant Indices

定义 \(d(x,k)\)\(x\)子树内到 \(x\) 距离为 \(k\) 的点的数量,求一个最小的 \(k\) 使得 每一个 \(x\) 最大。

维护一个最大数量的深度,更新时若数量大于当前深度对应的数量则更新,或者相等且深度较浅也更新,答案就是当前深度减去当前点的深度。


void dsu(int x,bool isson){
    for(auto y:v[x]){
        if(y==fa[x]||y==son[x])continue;
        dsu(y,false);
    }
    if(son[x])dsu(son[x],true);
    for(auto y:v[x]){
        if(y==fa[x]||y==son[x])continue;
        for(int i=ipt[y];i<=ipt[y]+sz[y]-1;i++){
            cnt[dep[mp[i]]]++;
            if(cnt[dep[mp[i]]]>cnt[now]||(cnt[dep[mp[i]]]==cnt[now]&&now>dep[mp[i]]))now=dep[mp[i]];
        }
    }
    cnt[dep[x]]++;
    if(cnt[dep[x]]>cnt[now]||(cnt[dep[x]]==cnt[now]&&now>dep[x]))now=dep[x];
    ans[x]=now-dep[x];
    if(isson==true)return;
    now=0;
    for(int i=ipt[x];i<=ipt[x]+sz[x]-1;i++)cnt[dep[mp[i]]]--;
}

CF741D Arpa’s letter-marked tree and Mehrdad’s Dokhtar-kosh paths

求每个点的子树内能够重排后形成回文串的简单路径的最大长度,边权为 \(a-v\).

边权是字符,可以看做 \(2^{ch-'a'}\) 来连边,合法的状态只有23种,为每个状态开一个桶,\(buc[x]\) 表示当前子树内,从 \(1\) 到子树内异或路径状态为 \(x\) 的节点的最大深度。

void dsu(int x,bool isson){
    for(int i=h[x];i;i=e[i].next){
        int y=e[i].to;
        if(y==son[x])continue;
        dsu(y,false);
        ans[x]=max(ans[x],ans[y]);
    }
    if(son[x])dsu(son[x],true),ans[x]=max(ans[x],ans[son[x]]);
    if(buc[dis[x]])ans[x]=max(ans[x],buc[dis[x]]-dep[x]);
    for(int i=0;i<22;i++)if(buc[dis[x]^1<<i])ans[x]=max(ans[x],buc[dis[x]^1<<i]-dep[x]);
    buc[dis[x]]=max(buc[dis[x]],dep[x]);
    for(int i=h[x];i;i=e[i].next){
        int y=e[i].to;
        if(y==son[x])continue;
        for(int j=ipt[y];j<=ipt[y]+sz[y]-1;j++){
            if(buc[dis[mp[j]]])ans[x]=max(ans[x],buc[dis[mp[j]]]+dep[mp[j]]-dep[x]*2);
            for(int k=0;k<22;k++)if(buc[dis[mp[j]]^1<<k])ans[x]=max(ans[x],buc[dis[mp[j]]^1<<k]+dep[mp[j]]-dep[x]*2);
        }
        for(int j=ipt[y];j<=ipt[y]+sz[y]-1;j++)buc[dis[mp[j]]]=max(buc[dis[mp[j]]],dep[mp[j]]);
    }
    if(isson==true)return;
    for(int i=ipt[x];i<=ipt[x]+sz[x]-1;i++)buc[dis[mp[i]]]=0;
}
posted @ 2022-11-16 21:10  半步蒟蒻  阅读(207)  评论(0)    收藏  举报