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

今天介绍一个神仙算法: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;
}

原文地址:https://www.cnblogs.com/light-house/p/11779076.html

时间: 2024-10-12 10:00:35

“优美的暴力”——树上启发式合并的相关文章

CodeForces 600E. Lomsat gelral【树上启发式合并】

传送门 好像大家都是拿这道题作为树上启发式合并的板子题. 树上启发式合并,英文是 dsu on tree,感觉还是中文的说法更准确,因为这个算法和并查集(dsu)没有任何关系.一般用来求解有根树的所有子树的统计问题. 根据轻重儿子的各种性质,可以证明这个算法的时间复杂度为 \(O(nlogn)\),虽然看起来暴力的不行,但是却是一个很高效的算法. 算法的核心其实就是对于每个节点,先计算轻儿子,再计算重儿子,把自己和轻儿子的所有贡献累计到重儿子上去,如果自己是轻儿子,就把贡献清空. 如果掌握了树链

图论-树上启发式合并(DSU On Tree)

Disjoint Set Union On Tree ,似乎是来自 Codeforces 的一种新操作,似乎被叫做"树上启发式合并". 在不带修改的有根树子树信息统计问题中,似乎树上莫队和这个 DSU On Tree 是两类常规操作. 先对树按轻重链剖分.对于每个节点,先计算轻儿子为根的子树信息,每次计算后消除影响,再去计算其他轻儿子.然后计算重儿子为根的子树信息,不消除影响,并把轻儿子们为根的子树信息加入,再合并这个节点本身的信息.由于一个大小 \(x\) 的子树被消除影响后,都把信

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

树上启发式合并是某些神仙题目的常见操作. 这里有一个讲得详细一点的,不过为了深刻记忆,我还是再给自己讲一遍吧! DSU(Disjoint Set Union),别看英文名挺高级,其实它就是并查集…… DSU on tree,也就是树上的启发式合并(众所周知,并查集最重要的优化就是启发式合并). 然后咱们来考虑一个基础题:给出一棵树,每个节点有颜色,询问一些子树中不同的颜色数量(颜色可重复).祖传数据($100000$). 当然,这道题可以被各种方法切,比如带修莫队(做法自行百度). 但莫队的时空

树上启发式合并入门

前言 树上启发式合并,即\(DSU\ on\ Tree\),是一个挺好用.挺实用的树上信息维护方法. 由于它比较简单,容易理解,因此这里也就简单记录一下吧. 前置知识:重儿子 什么是重儿子? 这应该是树链剖分中的一个概念吧.重儿子就是某个节点的子节点中,子树大小最大的节点. 适用情况 你可以很方便地给每个点染上白色和黑色,且你需要对于每个点都分别得到其子树内节点为黑.子树外节点为白的局面. 具体实现 这是一个比较贪心的过程. 考虑\(dfs\)遍历时,对于当前点的每个儿子,除最后操作的儿子以外,

【CF600E】Lomsat gelral——树上启发式合并

(题面来自luogu) 题意翻译 一棵树有n个结点,每个结点都是一种颜色,每个颜色有一个编号,求树中每个子树的最多的颜色编号的和. ci <= n <= 1e5 树上启发式合并裸题.统计时先扫一遍得到出现次数最大值,然后再扫一遍看哪个颜色的出现次数与mxCnt相等.注意用一个bool数组判重,清空轻儿子贡献时要顺手把bool数组也打成false. 代码: #include <iostream> #include <cstdio> #include <cctype&

CodeForces 375D. Tree and Queries【树上启发式合并】

传送门 题意 给出一棵 \(n\) 个结点的树,每个结点有一个颜色 \(c_i\) . 询问 \(q\) 次,每次询问以 \(v\) 结点为根的子树中,出现次数 \(\ge k\) 的颜色有多少种.树的根节点是 \(1\). 题解 反正我看见这个 \(\ge k\) 就觉得要用线段树,实际上好像不用写线段树的 Orz. 还是树上启发式合并,记录每种颜色出现的次数,然后线段树记录某种次数有多少颜色,更改就在线段树上改. 这是最后一道树上启发式合并的例题了,以后遇到再刷. #include <bit

HDU - 4358 Boring counting (树上启发式合并/线段树合并)

题目链接 题意:统计树上每个结点中恰好出现了k次的颜色数. dsu on tree/线段树合并裸题. 启发式合并1:(748ms) 1 #include<bits/stdc++.h> 2 using namespace std; 3 typedef long long ll; 4 const int N=1e5+10; 5 int n,m,k,a[N],b[N],nb,fa[N],son[N],siz[N],cnt[N],ans[N],now,ne,hd[N],ka; 6 struct E {

CF 600E 树上启发式合并

DUS on tree 难得都不会,会的都是板子,可悲,可悲 题意:略 先想一个O(n^2)的写法,然后想办法去掉重复计算.究竟哪里重复 了呢? 假设p是x的儿子,p有很多个.每次计算答案的时候,如果“重儿子”(子孙最多的p)的答案可以直接用的话, 就可以省去很多的重复计算,这就是书上启发式合并   DUS ON TREE写法类似板子(恕我无知,没见过其他整法) #include<cstdio> #include<vector> #include<algorithm>

树上启发式合并 (dsu on tree)

这个故事告诉我们,在做一个辣鸡出题人的比赛之前,最好先看看他发明了什么新姿势= =居然直接出了道裸题 参考链接: http://codeforces.com/blog/entry/44351(原文) http://blog.csdn.net/QAQ__QAQ/article/details/53455462 这种技巧可以在O(nlogn)的时间内解决绝大多数的无修改子树询问问题. 例1 子树颜色统计 有一棵n个点的有根树,根为1,每个点有一个1~n的颜色,对于每一个点给了一个数k,要询问这个子树