浅谈竞赛中的部分树(块、点、链)【长期更新】

[toc]
本文会从链剖讲起,涉及其变形进阶,之后也会讲到DFS序,树上分块,块状树,点分治与边分治,LCT,仙人掌与仙人球,伪top_tree(AAA树),top_tree。
本文重点讲述思维,至于例题虽会有涉及,但还是请读者自行百度(毕竟OI是不缺题的。。。。)
详略在文章中体现,如有错误,请各位读者指正。

从树链剖分讲起

其实我是先会LCT,再会链剖的,所以说比较尴尬。。。
但是自我学习了链剖后,我发现链剖给予了树上操作无限的可能性。

长链与重链

重链剖分在OI中很常见,所以这里只放出模板的博客:
感谢博主——Sdchr
长链剖分比较冷门,大部分使用在 O(1) 查询第k级祖先,与重链剖分相对的,就是把size的处理变为deep的处理。
这里也有份比较详细的blog: 感谢博主——ZZQ
这里的长链与重链的关系有些像平衡树中的重量平衡与深度平衡,各有特点,相互弥补在树上操作的不足。
重点即是重链与轻链的交替穿插,使得部分信息能够在树中快速上传,将树变得有序化,层次化就是树链剖分的本质。
长链剖分的题除上述提到外还有 BZOJ1785、BZOJ4543。
讲重点:
1.为什么要学树链剖分?
A:因为要考,lct的常数太大。链剖可以快速地与其他数据结构相关联,易出题。
2.如何学习链剖?
A:做题,做与其他数据结构、算法结合的题,特别是对于链剖在各类的树上运用。链剖模板很简单,但是如何运用是一个需要深度思考的问题。遇到链剖题千万不要裸上LCT水掉,当遇上卡常类、与一些高级算法结合类的题时就会无从下手。(说的就是blog主。。。)
3.学习链剖要到什么程度?
A:模板10~15min,重要的是快判断题目可以用链剖解决,并且知道如何解决(说白了,就是多刷点题)。

上干货(咦?长链剖分都不算干货?)
好吧。。补充长链剖分模板中的核心代码

int fa[N],depth[N],Dep[N],top[N],son[N],dfn[N],cnt;
inline void dfs(int u,int f){
    Dep[u]=depth[u]=depth[fa]+1,fa[u]=f;
    for(int p=head[u];p;p=G[p].next)
        if(V!=f)
            dfs(V,u),Dep[u]=max(Dep[u],Dep[V]);
}
inline void ask(int u,int fa,int z){
    dfn[u]=++cnt,top[u]=z,son[u]=0;
    int maxv=0;
    for(int p=head[u];p;p=G[p].next)
        if(V!=fa&&Dep[V]>maxv)
            maxv=Dep[son[u]=V];
    if(son[u])ask(son[u],u,z);
    for(int p=head[u];p;p=G[p].next)
        if(V!=fa&&V!=son[u])
    ask(V,u,V);
}

可以看见size不见了只多出来一个新的dep。剩下的就靠自己领悟了。

合并!

从刘rj的紫书上看到了有关树链剖分的合并,详情可以参考
%%%——idy002学长大佬。
有序链剖概念的提出给予我们一扇新世界的大门
见例题T3
link操作神来之笔,但是反过来说,难道寻常链剖做不到有序吗?不见得吧,用if特判或者再建立一棵倒着的树链剖分不就可以了吗?方法有很多不见得固定死思维,只要算法有正确性,复杂度合适,那么勇敢创新是值得借鉴的。

tips

链剖小技巧:
1.求LCA,虽然理论复杂度比倍增高,但是实际效果令人满意,链剖的理论常数基本不大于1,实际贴近于 12 ,所以LCA用它在线查询是很不错的。
2.长链、重链一起上,反正都要用到dfs序,而且步骤也简单只需追加一个数组即可,当你无法判定是重链还是长链时,这样很稳。

一般来说,裸的链剖是考不出什么花样的,所以学会套用数据结构才是灵活掌握链剖的表现。

过渡Ⅰ

链剖就这样结束了吧,毕竟大家都有过了解,并且也都学习过,接下来是一道裸题,轻喷。
BZOJ1036
没错,是一道已经做烂的经典题目,什么树链剖分+线段树就可以快速水过,详情请见hzwer(%%%)的代码,里面还有一份很清新的LCT的方法,这些都是极好的。
接下来,就是重点。为什么博主谈到了这道题,那是因为与接下来的块状树有关。

块状树(优秀的暴力)

一般谈到块状树,例题举例便是Gty的超级妹子树之类的,会使人感觉块状树引用范围太少,比不上链剖(虽然这是事实),块状树的应用实际上过更加广泛,更加灵活。
所以上面那道BZOJ1036便是一个例子。
这次不矫情,直接上code:

/*************************************************
    Problem: 1036
    User: lemonoil
    Language: C++
    Result: Accepted
    Time:2852 ms
    Memory:4512 kb
****************************************************/

#include<algorithm>
#include<cstdio>
#include<cmath>
const int maxn=30000,inf=~0U>>1;
using namespace std;
char cmd[10];
struct data{
    int to,next;
}E[maxn<<1],Et[maxn<<1];
int n,q,s,t,x,y,w[maxn],sqrtn,deep[maxn],fa[maxn];
int belong[maxn],M[maxn],sz[maxn],head[maxn],tail[maxn],tot,cnt;
void addedge(int x,int y){
    E[++tot].to=y,E[tot].next=head[x],head[x]=tot;
}
void insert(int x,int y){
    Et[++cnt].to=y,Et[cnt].next=tail[x],tail[x]=cnt;
}
void dfsf(int t,int f,int d){
    deep[t]=d,fa[t]=f;
    int tmp=belong[t];
    for(register int e=head[t];e;e=E[e].next)if(E[e].to!=f){
        if(sz[tmp]++<sqrtn)insert(t,E[e].to),belong[E[e].to]=tmp;
        dfsf(E[e].to,t,d+1);
    }
}
void dfss(int t,int s,int m){
    sz[t]=s+=w[t];M[t]=m=max(m,w[t]);
    for(register int e=tail[t];e;e=Et[e].next)dfss(Et[e].to,s,m);
}
void change(int t,int u){
    w[t]=u;
    if(t==belong[t])dfss(t,0,-inf);
    else dfss(t,sz[fa[t]],M[fa[t]]);
}
void query(int a,int b,int&s,int&m){
    s=0;m=-inf;
    while(a!=b){
        if(deep[a]<deep[b])swap(a,b);
        if(belong[a]==belong[b])
            s+=w[a],m=max(m,w[a]),a=fa[a];
        else{
            if(deep[belong[a]]<deep[belong[b]])swap(a,b);
            s+=sz[a],m=max(m,M[a]),a=fa[belong[a]];
        }
    }
    m=max(m,w[a]);s+=w[a];
}
inline void read(int &res){
    static char ch;int flag=1;
    while((ch=getchar())<'0'||ch>'9')if(ch=='-')flag=-1;res=ch-48;
    while((ch=getchar())>='0'&&ch<='9')res=res*10+ch-48;res*=flag;
}
int main(){
    read(n),sqrtn=sqrt(n)+1;
    for(register int i=0;i<n-1;i++)read(s),read(t),--s,--t,addedge(s,t),addedge(t,s);
    for(register int i=0;i<n;i++)read(w[i]),belong[i]=i;
    dfsf(0,-1,0),read(q);
    for(register int i=0;i<n;i++)if(belong[i]==i)dfss(i,0,-inf);
    for(register int i=0;i<q;i++){
        scanf("%s",cmd);
        read(s),read(t);
        if(cmd[0]=='C')change(s-1,t);
        else{
            query(s-1,t-1,x,y);
            if(cmd[1]=='S')printf("%d\n",x);
            else printf("%d\n",y);
        }
    }
    return 0;
}

直观感受就是简洁暴力,效率也是极其高的,是链剖的级别。但是时间复杂度理论却为 O(nn) 只能说明常数竟然为链剖级别!!
块状树的理论在这里 WJMZBMR(%%%)
就像论文里描述的,块状树目前只能解决单点、链的问题,针对子树问题就比较尴尬了。

博主不自量力,正在研究块状树对子树问题的解决方法。(多半是借助其他数据结构。。。)

建块的方法:Dfs一下,对每个结点只要其属于的块大小不超过SqrtN,就把其孩子加入其属于的块,否则其孩子自立一个块——from clj
这种建块方法严格保证块直径,大小,连通性,不保证块个数,
但是这种类似于SIZE的树上分块方法实际上效果很不错。

鉴于块状树的开发是否会有更大的进展目前暂时不明了,所以这里就此打住。在竞赛中,块状树在暴力的应用应该更显著,毕竟 n 的复杂度也确实比较大;

过渡Ⅱ

从块状树上面我们最应该学到的是其树上分块的思想,树上分块与树上分治是解决树上问题的利器。如何树上分块是依据题目特点
、要求所决定的。
所以下一章节便是
——

树上分块

大致上分为4类:

DFS序分块

通过DFS序每 N 个点分成一块,好写,非常有利于与
DFS 序有关的题目,且严格保证了块的大小,不保证块直径。

HEIGHT分块

通过Depth每 Hmax 分成一块,好写,严格保证块直径,
不保证块大小,保证连通。

SIZE分块

检验父亲所在的size是否小于 N ,小于就加入,否则新开
一块,严格保证块直径,大小,连通性,不保证块个数。

王室联邦分块

保证块直径,大小,个数,不保证联通。

归纳

上述4种方法各有优劣,一般说来DFS序、SIZE分块法比较常见,而王室联邦的分块方法则更像真正的高级树上分块方法,详情见下文。至于HEIGHT分块法。。。。我连例题都找不到一道,囧,知道有这样一种概念就好了吧。

BZOJ1086王室联邦

很有趣的题,鉴于普通的树上分块可以被菊花图开卡掉,所以我们可以考虑为其他的块预留一些位置,考虑到当一块size>=b时,就把这些分成一块。深度搜索时仍然是从任意节点开始,维护一个栈,当节点退出递归时便压栈,自上而下的合并至块中。所以当某子树深搜完后栈内元素>=b时便合并成一块。所以其缺点中的不保证联通性就是如果某子树深搜后栈内元素不够b,那么这些元素就会与下一个子树内部的节点合并,直至元素个数>=b。
所以我们每一次递归时维护栈底,对于当前子树,这个栈底即是整个栈的底,栈底下元素不可被修改、弹栈。最后保证了子树内未分分块的节点数<=b,并且每块的大小也不超过2b,将剩余节点暴力塞进最后一块中,那么最大的块就不会超过3b的size的大小。

重点: DFS序与SIZE

尽管有种种什么菊花图、链式图来卡树上分块,(那毕竟是少数无良出题人。。。。。),DFS序分块作为树问题转序列问题的经典方法,是很值得OIer学习的,考虑到维护时间戳,记录节点入度与出度的时间,可以轻松将树压缩与序列之上,剩下的什么分块就可以完美胜任了。

DFS序的运用

给定一颗树, 和每个节点的权值.下面有7个经典的关于dfs序的问题:

  1. 对某个节点X权值加上一个数W, 查询某个子树X里所有点权的和.
    由于X的子树在DFS序中是连续的一段, 只需要维护一个dfs序列,用树状数组实现:单点修改和区间查询.

  2. 对节点X到Y的最短路上所有点权都加一个数W, 查询某个点的权值.
    这个操作等价于
    a. 对X到根节点路径上所有点权加W
    b. 对Y到根节点路径上所有点权加W
    c. 对LCA(x, y)到根节点路径上所有点权值减W
    d. 对LCA(x,y)的父节点 fa(LCA(x, y))到根节点路径上所有权值减W
    于是要进行四次这样从一个点到根节点的区间修改.将问题进一步简化, 进行一个点X到根节点的区间修改, 查询其他一点Y时,只有X在Y的子树内, X对Y的值才有贡献且贡献值为W.当单点更新X时,X实现了对X到根的路径上所有点贡献了W.于是只需要更新四个点(单点更新) ,查询一个点的子树内所有点权的和(区间求和)即可.

  3. 对节点X到Y的最短路上所有点权都加一个数W, 查询某个点子树的权值之和.
    同问题2中的修改方法, 转化为修改某点到根节点的权值加/减W
    当修改某个节点A, 查询另一节点B时
    只有A在B的子树内, Y的值会增加
    W * (dep[A] - dep[B] + 1) => W * (dep [A] + 1) - W * dep[B]
    那么我们处理两个数组就可以实现:
    处理出数组Sum1,每次更新W*(dep[A]+1),和数组Sum2,每次更新W.
    每次查询结果为Sum1(R[B]) – Sum1(L[B]-1) - (Sum2(R[B]) – Sum2(L[B]-1)) * dep [B].

  4. 对某个点X权值加上一个数W, 查询X到Y路径上所有点权之和.
    求X到Y路径上所有的点权之和, 和前面X到Y路径上所有点权加一个数相似
    这个问题转化为
    X到根节点的和 + Y到根节点的和 - LCA(x, y)到根节点的和 - fa(LCA(x,y)) 到根节点的和
    更新某个点x的权值时,只会对它的子树产生影响,对x的子树的每个点到根的距离都加了W.
    那么我们用”刷漆”(差分前缀和),更新一个子树的权值.给L[x]加上W,给R[x]+1减去W,那么sum(1~L[k])就是k到根的路径点权和.

  5. 对节点X的子树所有节点加上一个值W, 查询X到Y的路径上所有点的权值和
    同问题4把路径上求和转化为四个点到根节点的和
    X到根节点的和 + Y到根节点的和 - LCA(x, y)到根节点的和 - parent(LCA(x,y)) 到根节点的
    再用刷漆只更新子树.
    修改一点A, 查询某点B到根节点时, 只有B在A的子树内, A对B才有贡献.
    贡献为W * (dep[B] - dep[A] + 1) => W * (1 - dep[A]) + W * dep[B]
    和第三题一样, 用两个sum1,sum2维护 W *(dep[A] + 1),和W.
    最后答案就是sum2*dep[B]-sum1.

  6. 对子树X里所有节点加上一个值W, 查询某个点的值.
    对DFS序来说, 子树内所有节点加W, 就是一段区间加W.
    所以这个问题就是 区间修改, 单点查询.树状数组+刷漆.
  7. 对子树X里所有节点加上一个值W, 查询某个子树的权值和.
    子树所有节点加W, 就是某段区间加W, 查询某个子树的权值和, 就是查询某段区间的和
    区间修改区间求和,用线段树可以很好解决。

以上引用自here
上面总结得十分详细,不可谓不全。
可以看出dfs序在树上操作中发挥了至关重要的作用,配合上线段树等其他数据结构,可以在基础树上操作内问题上纵横了。
但是dfs序最关键的是其思想:

树型转序列!

无论是线段树还是树状数组(两种烂大街但又无可替代的基础数据结构)都是序列型数据结构,能够转化为序列无疑是解决问题的一种方式。
回想树链剖分,不也是通过dfs将其编号然后植入到其他数据结构中解决吗?

size分块

这种分块方式就更加贴近于本身的树上分块的特点(在树上?):作用于树上。
举个例:Gty的妹子树(注意:没有“超级”)
这道题就是典型的size分块的题:
如果用王室联邦分块的话需要维护每一个块dfs的序最小值和最大值,并且插入操作会破坏原来的性质,所以很GG,不如直接按sizes分块,根节点size< block就加入根,否则新建块。
因为size分块不能保证块的数量,可以被菊花图卡掉,但是本题并没有所以就可以安心的写size,然后每个块维护排序后的值。
对于查询操作,不完整的块(因为是size分块所以只有子树根所在块不完整)暴力,直接把块重建一个图,每个块都整体二分。
修改维护有序,插入也维护有序,当然修改和插入后重新排序也可以.
复杂度 修改插入 O(S) 查询 O(S+NSlogS)

HEIGHT分块

我只能说无可奉告。。。。

过渡Ⅲ

那么下一章的方向也就很明显了,既然树分块都讲了,那么树分治肯定跑不了了,树的分块给人的感觉就是直接,在块内暴力,块上打标记,具体这么做有什么卵用。。。
花一点点篇幅谈谈吧,鉴于log级别的操作是极其强势的,所以思考分块比树类数据结构强在where? O(1) 查询历史,详细方式见BZOJ2589BZOJ4763

然后言归正传,边分治、点分治,我来了!!!

树分治

  • 4
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值