“优美的暴力”——树上启发式合并

Posted light-house

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了“优美的暴力”——树上启发式合并相关的知识,希望对你有一定的参考价值。

今天介绍一个神仙算法:Dsu On Tree[ 树上启发式合并 ]

这个算法用于离线处理询问子树信息,而且很好写。

但是在你没有理解它之前,这是个很鬼畜的算法。

理解后你才能真心感到它的美妙之处。

关键是它是有着媲美线段树合并的时间复杂度的“暴力”算法。

这里说一件事,我学这个东西时找了很多篇博客,它们无一例外地给出了这样一个流程:

1. 先统计一个节点所有的轻儿子 然后删除它的答案
2. 再统计这个节点的重儿子 保留他的答案
3. 再算一遍所有轻儿子 加到答案中上传

我当时就看的很懵逼,算一遍所有轻儿子,删掉,再算一遍,这不闲的?

直接统计它的重儿子再算轻儿子不就好了?很疑惑,问了身边很多人也都觉得迷惑。

人类迷惑行为大赏.jpg

后面我搞懂了,为了不让其他学习dsu on tree的人也觉得迷惑,我就写了这一篇博客。

在这里非常感谢洛谷两个dalao的帮助,现在理解了这个东西。

我们每次进入一棵子树计算答案时,都要把计算上一棵子树的数据清除。

为什么,如果我们带着上次计算后的结果去计算新子树,答案肯定是不对的。

但是,我们最后一棵子树不需要清除,因为我们不用再进入新子树了(没了)。

那我们再回到上面说的,为什么一开始要算一遍轻儿子?

从最纯粹的暴力开始,我们有两个函数dfs1和dfs2,dfs1函数作为主体函数,dfs2作为辅助函数。

先dfs1到每一个点,dfs1它的后代,计算后代的信息,再dfs2它的后代,计算自己的答案。

也就是说,开始算轻儿子是要把它后代的信息计算出来,而不是理解为之前提到那些博客里面的算出来答案后“删除答案”。删除答案是为了不让计算的数据冲突。

按照dalao说的,保留重儿子的信息可以优化复杂度。从原先暴力的O(n^2)优化到O(nlogn)。

所以为了不用清除重儿子的信息,先dfs1轻儿子,再dfs1重儿子,最后dfs2轻儿子更新自己的答案。

如果一开始没有dfs1轻儿子,我们就没有得到后代的信息,所谓“删除”答案,其实是从统计信息的数组把dfs1轻儿子时存进去的用于计算的“缓存”清理了。

下面给出代码:

#include<bits/stdc++.h>
#define N 100010
using namespace std;
inline int read(){
    int data=0,w=1;char ch=0;
    while(ch!=- && (ch<0||ch>9))ch=getchar();
    if(ch==-)w=-1,ch=getchar();
    while(ch>=0 && ch<=9)data=data*10+ch-0,ch=getchar();
    return data*w;
}
struct Edge{
    int nxt,to;
    #define nxt(x) e[x].nxt
    #define to(x) e[x].to
}e[N<<1];
int head[N],tot;
inline void addedge(int f,int t){
    nxt(++tot)=head[f];to(tot)=t;head[f]=tot;
}
int cnt[N],siz[N],son[N],c[N],max_val,n,child;
long long sum,ans[N];
void add(int x,int f,int val){
    cnt[c[x]]+=val;
    if(cnt[c[x]]>max_val)max_val=cnt[c[x]],sum=c[x];
    else if(cnt[c[x]]==max_val)sum+=1LL*c[x];
    for(int i=head[x];i;i=nxt(i)){
        int y=to(i);
        if(y==f||y==child)continue;
        add(y,x,val);
    }
}
void dfs1(int x,int f){//重链剖分
    siz[x]=1;int maxson=-1;
    for(int i=head[x];i;i=nxt(i)){
        int y=to(i);
        if(y==f)continue;
        dfs1(y,x);
        siz[x]+=siz[y];
        if(siz[y]>maxson){
            maxson=siz[y];son[x]=y;
        }
    }
}
void dfs2(int x,int f,int opt){//opt为0表示统计后的答案要删掉,opt为1则不用删
    for(int i=head[x];i;i=nxt(i)){
        int y=to(i);
        if(y==f)continue;
        if(y!=son[x])dfs2(y,x,0);
    }if(son[x])dfs2(son[x],x,1),child=son[x];
    add(x,f,1);child=0;
    ans[x]=sum;
    if(!opt)add(x,f,-1),sum=0,max_val=0;
}
int main(){
    n=read();
    for(int i=1;i<=n;i++)c[i]=read();
    for(int i=1;i<n;i++){
        int x=read(),y=read();
        addedge(x,y);addedge(y,x);
    }
    dfs1(1,0);dfs2(1,0,0);
    for(int i=1;i<=n;i++)
        printf("%lld ",ans[i]);
    return 0;
}

以上是关于“优美的暴力”——树上启发式合并的主要内容,如果未能解决你的问题,请参考以下文章

CF600ELomsat gelral——树上启发式合并

#线段树合并树上启发式合并#CF600E Lomsat gelral

树上启发式合并/dsu on tree

CF 600 E Lomsat gelral —— 树上启发式合并

CF600ELomset gelral 题解(树上启发式合并)

基本操作树上启发式合并の详解