dsu on tree及其等价做法

前言

本 MnZn 对 dsu on tree 的理解一直比较浅显,幸得大神 0x3b800001 帮助,发现 dsu on tree 有一百万种写法,故作此篇。

大佬给的例题

这题其实最多绿到蓝吧,我不清楚啊。下面就以这题代码为例,大概总结下各种写法。

正统 dsu

我们只用一个数组来实现这一过程。(代码中的 \(f\)

考虑走到一个点时,每次先将其所有轻儿子答案计算完,并清空 \(f\) 数组再计算重儿子答案,并将信息存储在 \(f\) 中。此时不清空 \(f\),再扫一遍轻儿子,并把轻儿子的信息合并在 \(f\) 上。这一遍历只需暴力扫子树内的每一个点并将信息放入 \(f\) 中,而无需上述对轻重儿子的区分。

处理完 \(f\) 数组,我们即可算出该点的答案。注意,我们不可以暴力扫 \(f\) 数组,而是使用一个 \(nowmx\) 来记录对于 \(f\) 当前子树内是同一统治颜色的点的数量,并用 \(nowans\) 来记录当前 \(f\) 对应的答案。这样可以保证时间复杂度为 \(O(n\log n)\)

复杂度分析

由上述过程我们可以看出,每个点被扫的次数等于其到根节点上的轻边条数 \(+1\)。然后这个次数我们知道是 \(\log n\) 级别的。所以时间复杂度为 \(O(n\log n)\)

空间复杂度为 \(O(n)\)

代码

#include<cstdio>
#include<iostream>
#define int long long
using namespace std;
const int mn=1e5+5;
int n;
int ans[mn],a[mn];
// int t[mn];
int son[mn],sz[mn],f[mn];
int vis[mn],nowv;
int nowmx,nowans;
int hd[mn],to[mn<<1],nxt[mn<<1],cnt=2;
void add(int x,int y)
{
    nxt[cnt]=hd[x];
    to[cnt]=y;
    hd[x]=cnt++;
}
void init(int x,int y)
{
    sz[x]=1;
    for(int i=hd[x];i;i=nxt[i])
    {
        int u=to[i];
        if(u==y)continue;
        init(u,x);
        sz[x]+=sz[u];
        if(sz[u]>sz[son[x]])
        {
            son[x]=u;
        }
    }
}
//tj=1 need to keep the val for future use
void dfs(int x,int y,bool tj)
{
    // cerr<<x<<" "<<son[x]<<" "<<tj<<'\n';
    // for(int )
    for(int i=hd[x];i;i=nxt[i])
    {
        int u=to[i];
        if(u==y)continue;
        if(!ans[x] && u==son[x])continue;
        dfs(u,x,ans[x]);
    }
    if(ans[x])
    {
        if(vis[a[x]]<nowv)
        {
            f[a[x]]=0;
            vis[a[x]]=nowv;
        }
        if((++f[a[x]])>nowmx)
        {
            nowans=a[x];
            nowmx=f[a[x]];
        }
        else if(f[a[x]]==nowmx)
        {
            nowans+=a[x];
        }
        return;
    }
    if(son[x])dfs(son[x],x,1);
    for(int i=hd[x];i;i=nxt[i])
    {
        int u=to[i];
        if(u==y)continue;
        if(u==son[x])continue;
        dfs(u,x,1);
    }
    if(vis[a[x]]<nowv)
    {
        f[a[x]]=0;
        vis[a[x]]=nowv;
    }
    if((++f[a[x]])>nowmx)
    {
        nowans=a[x];
        nowmx=f[a[x]];
    }
    else if(f[a[x]]==nowmx)
    {
        nowans+=a[x];
    }
    // cerr<<x<<" "<<nowmx<<'\n';
    ans[x]=nowans;
    if(!tj)
    {
        ++nowv;
        nowans=0;
        nowmx=0;
    }
}
signed main()
{
    int x,y;
    scanf("%lld",&n);
    for(int i=1;i<=n;i++)
    {
        scanf("%lld",&a[i]);
    }
    for(int i=1;i<n;i++)
    {
        scanf("%lld%lld",&x,&y);
        add(x,y);
        add(y,x);
    }
    init(1,0);
    dfs(1,0,0);
    for(int i=1;i<=n;i++)
    {
        printf("%lld ",ans[i]);
    }
}

神秘 STL 合并

对每个点开一个 unordered_map (视情况而定,该题用这个比较好),存子树内的颜色信息。处理完子树内的后,将 umap 给父亲合并,合并后清空该 umap 以保证空间不爆炸。

仍然要分轻重儿子。每个非叶子节点先处理重儿子答案,然后直接继承重儿子的 umap,向其中加入自己的颜色并更新答案。随后扫轻儿子,并将轻儿子的 umap 暴力并自己的 umap 上。

对于答案统计,可参考上一 part 所述方法。时间复杂度 \(O(n\log n)\)

复杂度分析

考虑最劣情况,一定是每个点颜色都不同。此时相当于每个点的信息被合并的次数等同于到根节点路径上轻边数 \(+1\),所以时间复杂度 \(O(n\log n)\)

每个点的信息最多同时存在于 \(O(1)\) 个 umap 中(刚合并而未清空其中一个时),所以空间复杂度是 \(O(n)\) 的。

代码

#include<cstdio>
#include<unordered_map>
#define int long long
#define fi first
#define se second
using namespace std;
const int mn=1e5+5;
int hd[mn],to[mn<<1],nxt[mn<<1],cnt=2;
int n,a[mn];
int ans[mn],tc[mn];
int son[mn],sz[mn];
void add(int x,int y)
{
    nxt[cnt]=hd[x];
    to[cnt]=y;
    hd[x]=cnt++;
}
unordered_map<int,int> mp[mn];
void init(int x,int y)
{
    sz[x]=1;
    for(int i=hd[x];i;i=nxt[i])
    {
        int u=to[i];
        if(u==y)continue;
        init(u,x);
        sz[x]+=sz[u];
        if(sz[u]>sz[son[x]])
        {
            son[x]=u;
        }
    }
}
void dfs(int x,int y)
{
    if(son[x])
    {
        dfs(son[x],x);
        ans[x]=ans[son[x]];
        tc[x]=tc[son[x]];
        swap(mp[x],mp[son[x]]);
        if((++mp[x][a[x]])>tc[x])
        {
            ans[x]=a[x];
            tc[x]=mp[x][a[x]];
        }
        else if(mp[x][a[x]]==tc[x])
        {
            ans[x]+=a[x];
        }
    }
    else
    {
        tc[x]=1;
        ans[x]=a[x];
        mp[x][a[x]]=1;
        return;
    }
    for(int i=hd[x];i;i=nxt[i])
    {
        int u=to[i];
        if(u==y || u==son[x])continue;
        dfs(u,x);
        for(auto j:mp[u])
        {
            if((mp[x][j.fi]+=j.se)>tc[x])
            {
                ans[x]=j.fi;
                tc[x]=mp[x][j.fi];
            }
            else if(mp[x][j.fi]==tc[x])
            {
                ans[x]+=j.fi;
            }
        }
        mp[u].clear();
    }
}
signed main()
{
    int x,y;
    scanf("%lld",&n);
    for(int i=1;i<=n;i++)
    {
        scanf("%lld",&a[i]);
    }
    for(int i=1;i<n;i++)
    {
        scanf("%lld%lld",&x,&y);
        add(x,y);
        add(y,x);
    }
    init(1,0);
    dfs(1,0);
    for(int i=1;i<=n;i++)
    {
        printf("%lld ",ans[i]);
    }
}

对于每个重链统计答案

我觉得这个说实话不该放到这里面来,但是复杂度和原理都是相同的,这里因为个人原因懒得写了不给出代码。

我们每次取出一条重链,然后从链尾到链头暴力计算子树内的答案。每个点最多被访问次数和头上的重链个数相等,而两条重链过渡必须以一条轻边分割,说白了还是一样的原理,时间复杂度还是 \(O(n\log n)\)

posted @ 2025-02-13 20:51  ikusiad  阅读(36)  评论(0)    收藏  举报