树上启发式合并,静态链分治(DSU)

本文详细介绍了树上启发式合并(DSU)算法在处理树形结构问题中的应用,包括统计共同祖先、判断子树是否构成回文串、查找主要颜色、计算颜色出现次数以及寻找距离最大节点。通过实例代码展示如何实现这些操作,并提供了多个在线判题链接作为练习。
摘要由CSDN通过智能技术生成


一,讲解

https://www.acwing.com/solution/content/59241/

  • 遍历每一个节点
  • 递归解决所有的轻儿子,同时消除递归产生的影响
  • 递归重儿子,不消除递归的影响
  • 统计所有轻儿子对答案的影响
  • 更新该节点的答案
  • 删除所有轻儿子对答案的影响

二,使用

T1

题目

一片森林,每次询问一个点与多少个点拥有共同的K级祖先

友好链接

https://www.luogu.com.cn/problem/CF208E

https://www.luogu.com.cn/blog/defKaeru/solution-cf208e

题解

我们首先考虑在一棵树上处理的情况,把问题转换一下,其实让你求的就是以某个节点为根的子树中的深度为k的节点个数

因此我们可以用倍增法先求出每一个节点的K级祖先,然后再在第k级祖先上处理维护答案就行了。

code

#include<bits/stdc++.h>
#define rep(i,j,k) for(int i=j;i<=k;i++)
#define pb push_back
using namespace std;
typedef long long ll;
typedef pair<int,int>pii;
const int inf= 0x3f3f3f3f;
const int N = 5e5+9;
int n,m;
int dep[N],sz[N],hson[N];
int cnt[N],ans[N],color[N];
int vis[N];
int f[N][26];
vector<int>g[N];
vector<pii>Q[N];


template <typename T> void inline read(T &x)
{
    x= 0 ; bool f= 0; char ch  = getchar();
    while (ch < '0' || ch > '9') {if(ch == '-')f= 1; ch = getchar();}
    while (ch <='9' && ch >= '0') {x= (x<<1)+ (x<<3) + ch - '0'; ch = getchar();}
    if(f)x*=-1;
}

void dfs_tree(int x,int fa)
{
    dep[x]=dep[fa]+1;
    sz[x]= 1;
    f[x][0]=fa;
    rep(i,1,20) f[x][i]=f[f[x][i-1]][i-1];
    for (auto j:g[x])
    {
        if(j==fa)continue;
        dfs_tree(j,x);
        sz[x]+=sz[j];
        if(sz[hson[x]]<sz[j])hson[x]=j;
    }
}

void calc (int u,int f,int k)
{
    /// 统计整个子树的信息
    cnt[dep[u]]+=k;
    for(auto v:g[u])
    {
        if(v!=f&&!vis[v])///不反祖且未被访问
        {
            calc(v,u,k);
        }
    }
}

void dfs_dsu(int u,int f,int keep)
{
    for(auto v:g[u])
    {
        if(v!=hson[u]&&v!=f)
            {
                dfs_dsu(v,u,0);
            }
    }
        /// 递归处理子树问题

    if(hson[u])dfs_dsu(hson[u],u,1),vis[hson[u]]=1;
    calc(u,f,1);///统计非重子树的信息
    for(int i= 0;i < Q[u].size() ; i++)
    {
        int id = Q[u][i].first;  ///询问编号
        int d = Q[u][i].second;  ///询问深度
        ans[id]=cnt[d]-1;
    }
    if(hson[u])vis[hson[u]]=0;
    if(keep==0)calc(u,f,-1);
}

void pre()
{
    for (int k = 1;k <= 26 ; k ++)
    {
         for (int i = 1; i <= n ; i++) f[i][k]=f[f[i][k-1]][k-1];
    }
}


int getfa(int x,int lev)
{
    for(int i=20;~i;i--)
    {
        if(lev&(1<<i))x=f[x][i];
    }
    return x;
}

int main()
{
    read(n);
    rep(i,1,n)
    {
        int x; read(x);
        if(x) g[x].pb(i),g[i].pb(x);
    }

    read(m);
    rep(i,1,n)if(!dep[i])dfs_tree(i,0);

    rep(i,1,m)
    {
        int v,p;
        read(v); read(p);
        int fax= getfa(v,p); ///获取v的p级祖先
        Q[fax].pb({i,dep[v]});
    }
    rep(i,1,n)if(dep[i]==1)dfs_dsu(i,0,0);
    rep(i,1,m)printf("%d\n",ans[i]);
    return 0;
}

T2

题目

给定一个以1为根的n个节点的树,每个点上有一个字母(a-z),

每个点的深度定义为该节点到1号节点路径上的点数.

每次询问 a,b.查询以a为根的子树内深度为b的节点上的字母重新排列之后是否能构成回文串.

友好链接

https://www.luogu.com.cn/problem/CF570D

题解

T1的cnt基础上再加上一维颜色

重排之后构成回文串的条件是:每个字母出现次数都为偶数或者只有一个字母出现次数为奇数。

因此我们可以直接维护cnt数组来统计子树中在某个深度上每个字符出现次数,然后再检查是否符合条件就行了。

至于查询操作,我们可以离线处理,然后把它挂在节点上,每当统计到该节点时,更新答案就行了。

code

#include<bits/stdc++.h>
#define rep(i,j,k) for(int i=j;i<=k;i++)
#define pb push_back
using namespace std;
typedef long long ll;
typedef pair<int,int>pii;
const int inf= 0x3f3f3f3f;
const int N = 5e5+9;
int n,m;
int dep[N],sz[N],hson[N];
int cnt[N][30],ans[N],color[N];
int vis[N];
vector<int>g[N];
vector<pii>Q[N];


template <typename T> void inline read(T &x)
{
    x= 0 ; bool f= 0; char ch  = getchar();
    while (ch < '0' || ch > '9') {if(ch == '-')f= 1; ch = getchar();}
    while (ch <='9' && ch >= '0') {x= (x<<1)+ (x<<3) + ch - '0'; ch = getchar();}
    if(f)x*=-1;
}

void dfs_tree(int x,int fa)
{
    dep[x]=dep[fa]+1;
    sz[x]= 1;
    for (auto j:g[x])
    {
        if(j==fa)continue;
        dfs_tree(j,x);
        sz[x]+=sz[j];
        if(sz[hson[x]]<sz[j])hson[x]=j;
    }
}

void calc (int u,int f,int k)
{
    /// 统计整个子树的信息
    cnt[dep[u]][color[u]]+=k;
    for(auto v:g[u])
    {
        if(v!=f&&!vis[v])///不反祖且未被访问
        {
            calc(v,u,k);
        }
    }
}

void dfs_dsu(int u,int f,int keep)
{
    for(auto v:g[u])
    {
        if(v!=hson[u]&&v!=f)
            {
                dfs_dsu(v,u,0);
            }
    }
        /// 递归处理子树问题

    if(hson[u])dfs_dsu(hson[u],u,1),vis[hson[u]]=1;
    calc(u,f,1);///统计非重子树的信息
    for(int i= 0;i < Q[u].size() ; i++)
    {
        int id = Q[u][i].first;  ///询问编号
        int d = Q[u][i].second;  ///询问深度
        int num = 0;

        for(int j = 1;j<= 26;j++)
            if(cnt[d][j]&1) num++;

        ans[id]=num>1?0:1;
    }
    if(hson[u])vis[hson[u]]=0;
    if(keep==0)calc(u,f,-1);
}

int main()
{
    read(n); read(m);
    rep(i,2,n)
    {
        int x; read(x);
        g[x].pb(i); g[i].pb(x);
    }
    char s[N];
    scanf("%s",s+1);
    int len = strlen(s+1);
    rep(i,1,len)color[i]=s[i]-'a'+1;
    dfs_tree(1,0);
    rep(i,1,m)
    {
        int x,y;
        read(x); read(y);
        Q[x].pb({i,y});
    }
    dfs_dsu(1,0,1);
    rep(i,1,m)
    {
        if(ans[i])puts("Yes");
        else puts("No");
    }
    return 0;
}

T3

题目

给定一棵由 n 个节点组成的树。

树的节点编号为 1∼n,其中 1 号节点为树的根节点。

每个节点上都标有某种颜色。

第 i 个节点上的颜色为 ci。

如果在以节点 v 为根节点的子树中,没有任何颜色的出现次数超过颜色 c 的出现次数,那么我们称颜色 c 为该子树的主要颜色。

一些子树的主要颜色可能不止一种。

以节点 v 为根节点的子树由节点 v 以及到达根节点的路径中包含节点 v 的其它所有节点共同组成。

对于每个节点 v(1≤v≤n),请你求出以该节点为根节点的子树的所有主要颜色之和。

友好链接

https://www.luogu.com.cn/problem/CF600E

题解

  • 注意统计答案的时候不用遍历cnt数组,可以在加减档过程中维护
  • 注意开ll
  • 注意不要忘记开重链剖分

code

#include<bits/stdc++.h>
#define rep(i,j,k) for(int i=j;i<=k;i++)
#define pb push_back
using namespace std;
typedef long long ll;
typedef pair<int,int>pii;
const int inf= 0x3f3f3f3f;
const int N = 5e5+9;
int n,m;
int dep[N],sz[N],hson[N];
ll cnt[N],ans[N];
int col[N];
int vis[N];
int f[N][26];
vector<int>g[N];
vector<pii>Q[N];
ll sum,mx;

template <typename T> void inline read(T &x)
{
    x= 0 ; bool f= 0; char ch  = getchar();
    while (ch < '0' || ch > '9') {if(ch == '-')f= 1; ch = getchar();}
    while (ch <='9' && ch >= '0') {x= (x<<1)+ (x<<3) + ch - '0'; ch = getchar();}
    if(f)x*=-1;
}

void dfs_tree(int x,int fa)
{
    dep[x]=dep[fa]+1;
    sz[x]= 1;
    for (auto j:g[x])
    {
        if(j==fa)continue;
        dfs_tree(j,x);
        sz[x]+=sz[j];
        if(sz[hson[x]]<sz[j])hson[x]=j;
    }
}

void calc (int u,int f,int k)
{
    /// 统计整个子树的信息
    cnt[col[u]]+=k;

    if(cnt[col[u]]>mx)sum=col[u],mx=cnt[col[u]];
    else if(cnt[col[u]]==mx)sum+=col[u];


    for(auto v:g[u])
    {
        if(v!=f&&!vis[v])///不反祖且未被访问
        {
            calc(v,u,k);
        }
    }
}

void dfs_dsu(int u,int f,int keep)
{
    for(auto v:g[u])
    {
        if(v!=hson[u]&&v!=f)
            {
                dfs_dsu(v,u,0);
            }
    }
        /// 递归处理子树问题

    if(hson[u])dfs_dsu(hson[u],u,1),vis[hson[u]]=1;
    calc(u,f,1);///统计非重子树的信息
    ans[u]=sum;
    if(hson[u])vis[hson[u]]=0;
    if(keep==0)calc(u,f,-1),mx=sum= 0;
}


int main()
{
    read(n);

    rep(i,1,n)read(col[i]);

    rep(i,1,n-1)
    {
        int v,p;
        read(v); read(p);
        g[v].pb(p); g[p].pb(v);
    }
    dfs_tree(1,0);
    dfs_dsu(1,0,0);
    rep(i,1,n)printf("%lld ",ans[i]);
    return 0;
}

T4

题目

给出一棵 n 个结点的树,每个结点有一个颜色 c i 。 询问 q 次,每次询问以 v 结点为根的子树中,出现次数 ≥k 的颜色有多少种。树的根节点是1.

友好链接

https://www.luogu.com.cn/problem/CF375D

题解

维护一个后缀和,我们结合calc函数看一下框架

考虑到出现次数k最大不超过点数,开一个桶是可行的

num[x] 表示出现次数大于等于 k 的次数

考虑到增加的时候是一个一个加的,删除的时候,每删除一个也是只对删除的当前k造成了影响,只要处理这一位就好了,不用管前面的或者后面的

void calc (int u,int f,int k)
{
    int pos = col[u];
    /// num[x] 表示出现次数大于等于k的计数
    if(k==-1) num[cnt[pos]]+=k;

    cnt[pos]+=k;
    if(k==1) num[cnt[pos]]+=k;

    for (auto v:g[u])
    {
        if(v!=f&&!vis[v])calc(v,u,k);
    }
}

code

#include<bits/stdc++.h>
#define rep(i,j,k) for(int i=j;i<=k;i++)
#define pb push_back
using namespace std;
typedef long long ll;
typedef pair<int,int>pii;
const int inf= 0x3f3f3f3f;
const int N = 5e5+9;
int n,m;
int dep[N],sz[N],hson[N];
int cnt[N],ans[N];
int col[N];
int vis[N];
int num[N];
vector<int>g[N];
vector<pii>Q[N];
int colmax;

template <typename T> void inline read(T &x)
{
    x= 0 ; bool f= 0; char ch  = getchar();
    while (ch < '0' || ch > '9') {if(ch == '-')f= 1; ch = getchar();}
    while (ch <='9' && ch >= '0') {x= (x<<1)+ (x<<3) + ch - '0'; ch = getchar();}
    if(f)x*=-1;
}

void dfs_tree(int x,int fa)
{
    dep[x]=dep[fa]+1;
    sz[x]= 1;
    for (auto j:g[x])
    {
        if(j==fa)continue;
        dfs_tree(j,x);
        sz[x]+=sz[j];
        if(sz[hson[x]]<sz[j])hson[x]=j;
    }
}

void calc (int u,int f,int k)
{
    int pos = col[u];
    /// num[x] 表示出现次数大于等于k的计数
    if(k==-1) num[cnt[pos]]+=k;

    cnt[pos]+=k;
    if(k==1) num[cnt[pos]]+=k;

    for (auto v:g[u])
    {
        if(v!=f&&!vis[v])calc(v,u,k);
    }
}

void dfs_dsu(int u,int f,int keep)
{
    for(auto v:g[u])
    {
        if(v!=hson[u]&&v!=f)
            {
                dfs_dsu(v,u,0);
            }
    }
        /// 递归处理子树问题

    if(hson[u])dfs_dsu(hson[u],u,1),vis[hson[u]]=1;
    calc(u,f,1);///统计非重子树的信息
    for (int i= 0;i<Q[u].size();i++)
    {
        ll sum =0;
        int id = Q[u][i].first;
        int d = Q[u][i].second;
        ans[id]=num[d];
    }

    if(hson[u])vis[hson[u]]=0;
    if(keep==0)calc(u,f,-1);
}
int main()
{
    read(n);read(m);
    rep(i,1,n)read(col[i]),colmax=max(colmax,col[i]);
    rep(i,1,n-1)
    {
        int a,b;
        read(a); read(b);
        g[b].pb(a);
        g[a].pb(b);
    }

    dfs_tree(1,0);
    rep(i,1,m)
    {
        int v,p;
        read(v);read(p);
        Q[v].pb({i,p});
    }

    dfs_dsu(1,0,1);
    rep(i,1,m)printf("%d\n",ans[i]);

    return 0;
}


T5

题目

给定一棵以 1 为根,n 个节点的树。设 d(u,x) 为 u 子树中到 u 距离为 x 的节点数。

对于每个点,求一个最小的 k,使得 d(u,k) 最大。

友好链接

https://www.luogu.com.cn/problem/CF1009F

题解

注意答案统计方式,在注销非重儿子的时候,可以认为开始新的问题,答案的状态可设置为初始状态

void calc(int u,int f,int k)
{
    cnt[dep[u]]+=k;
    if(k>0&&cnt[dep[u]]>=maxcnt)
    {
        if(cnt[dep[u]]>maxcnt)maxcnt=cnt[dep[u]],mindep=dep[u];
        else if(cnt[dep[u]]==maxcnt)mindep=min(mindep,dep[u]);
    }

    for(auto v:g[u])
    {
        if(v!=f&&!vis[v])calc(v,u,k);
    }
}

code

#include<bits/stdc++.h>
#define rep(i,j,k) for(int i=j;i<=k;i++)
#define pb push_back
using namespace std;
typedef long long ll;
typedef pair<int,int>pii;
const int inf= 0x3f3f3f3f;
const int N = 1e6+9;
int n,m;
int dep[N],sz[N],hson[N];
int cnt[N],ans[N];
int vis[N];
vector<int>g[N];
vector<pii>Q[N];
int mindep,maxcnt;

template <typename T> void inline read(T &x)
{
    x= 0 ; bool f= 0; char ch  = getchar();
    while (ch < '0' || ch > '9') {if(ch == '-')f= 1; ch = getchar();}
    while (ch <='9' && ch >= '0') {x= (x<<1)+ (x<<3) + ch - '0'; ch = getchar();}
    if(f)x*=-1;
}

void dfs_tree(int u,int f)
{
    sz[u]= 1;
    dep[u]=dep[f]+1;
    for(auto v:g[u])
    {
        if(v==f)continue;
        dfs_tree(v,u);
        sz[u]+=sz[v];
        if(sz[hson[u]]<sz[v])hson[u]=v;
    }
}

void calc(int u,int f,int k)
{
    cnt[dep[u]]+=k;
    if(k>0&&cnt[dep[u]]>=maxcnt)
    {
        if(cnt[dep[u]]>maxcnt)maxcnt=cnt[dep[u]],mindep=dep[u];
        else if(cnt[dep[u]]==maxcnt)mindep=min(mindep,dep[u]);
    }

    for(auto v:g[u])
    {
        if(v!=f&&!vis[v])calc(v,u,k);
    }
}

void dfs_dsu(int u,int f,int keep)
{
    for(auto v:g[u])
    {
        if(v!=hson[u]&&v!=f) dfs_dsu(v,u,0);
    }
    if(hson[u])dfs_dsu(hson[u],u,1),vis[hson[u]]= 1;
    calc(u,f,1);
    ans[u]=mindep-dep[u];
    if(hson[u])vis[hson[u]]=0;
    if(keep==0)calc(u,f,-1),mindep=inf,maxcnt=0;
}

int main()
{
    read(n);
    rep(i,1,n-1)
    {
        int a,b;
        read(a); read(b);
        g[a].pb(b); g[b].pb(a);
    }
    dfs_tree(1,0);
    dfs_dsu(1,0,1);
    rep(i,1,n)printf("%d\n",ans[i]);
    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

流苏贺风

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值