zoukankan      html  css  js  c++  java
  • 【转】动态树:实现

    我最近看到zjoi2011的一道题:

    http://www.zybbs.org/JudgeOnline/problem.php?id=2325

    之后一惊:这不是传说中的动态树吗,怎么都出到省选里了?

    我又看到了某神牛的博文:

    http://hi.baidu.com/wjbzbmr/blog/item/83f31646fd360554500ffecd.html

    “不过我权衡了一下,觉得树链剖分我几乎写过10多次了。。应该还是写的出来的。。”

    我被震撼了:真是人在北京好似坐井观天,人家都写了10遍的东西我竟然还认为OI中不会考呢!

    于是,我痛下决心:疯狂练习,攻克动态树。

    动态树除了上面的那题外,还有

    http://www.zybbs.org/JudgeOnline/problem.php?id=1036

    也是zjoi的题。

    以及spoj上的QTREE。

    http://www.spoj.pl/problems/QTREE/

    我决定就把这三道题刷了好了。

    动态树的实现主要有5种:

    link-cut tree *

    Euler-Tour tree

    全局平衡二叉树 *

    树链剖分

    树块剖分 *

    我们重点关注带星号的实现

    link-cut tree的思想是把树剖分成若干个链,不过这些链是用splay动态维护的,每次查询的时候把一个点到根的路径连成一个链。几篇入门文章可以到这里下载:

    http://cid-354ed8646264d3c4.office.live.com/self.aspx/.Public/DynamicTree/QTREE^_YangZhe.pdf

    http://cid-354ed8646264d3c4.office.live.com/view.aspx/.Public/DynamicTree/CollectionOfAlgorithms^_DynamicTree.doc

    一些实现上的技巧可以参考杨哲的文章,我综合以上两篇文章得出了我自己的写法:

    http://cid-354ed8646264d3c4.office.live.com/self.aspx/.Public/DynamicTree/2325^_2.cpp

    总体来说,是融合了朴素与飘逸。程序100多行,4K,还行。最核心的连接操作非常经典:

    node *Expose(node *p){

    node *q;

    for (q=NULL;p;p=p->f){

    Splay(p);

    p->r=q;

    (q=p)->update();

    }

    return q;

    }

    Splay操作则是唐文斌教给我的写法(融合了杨哲的改进):

    void Splay(node *p){

    while (p->f && (p->f->l==p || p->f->r==p)){

    node *q=p->f,*y=q->f;

    if (y && y->l==q){

    if (q->l==p)zig(q),zig(p);

    else zag(p),zig(p);

    }else if (y && y->r==q){

    if (q->r==p)zag(q),zag(p);

    else zig(p),zag(p);

    }else{

    if (q->l==p)zig(p);

    else zag(p);

    }

    }

    p->update();

    }

    这些代码其实非常优雅、流畅,默写一遍20分钟足矣,我写到第2遍的时候就已经不用调试直接正确了。

    SPOJ上QTREE那题用这个模板AC没有阻碍:

    http://cid-354ed8646264d3c4.office.live.com/self.aspx/.Public/DynamicTree/375.cpp

    可以说,以后考试的时候遇到动态树我就写Link-Cut Tree了。

    Link-Cut Tree的常数其实很糟糕,这主要是splay导致的。

    改进常数的方法是建立一棵”全局平衡二叉树“,也是树链剖分,不过把整个树看做一体,修改每个链选择根节点的规则,使得任何一个节点的深度不超过2logN。具体见杨哲的文章。

    我很纠结地写出来了代码。

    http://cid-354ed8646264d3c4.office.live.com/self.aspx/.Public/DynamicTree/2325^_3.cpp

    说它纠结,其实倒不是建树的过程有多麻烦,相反,非常容易。恶心的是查询的写法。

    我写的第一个版本跑得比link-cut tree还慢。经过参考各种代码之后,我终于找到了正确、高效的写法:

    int Ask(int x,int y){

    rec left=rec::empty(),right=rec::empty();

    while (head[x]!=head[y]){

    if (depth[head[x]]>depth[head[y]]){

    for (int b=rc[x],i=x;i!=-1;i=tf[i]){

    if (b==rc[i]){

    left=left+R[i];

    if (lc[i]!=-1)left=left+S[lc[i]];

    }

    b=i;

    }

    x=fa[head[x]];

    }else{

    for (int b=rc[y],i=y;i!=-1;i=tf[i]){

    if (b==rc[i]){

    right=right+R[i];

    if (lc[i]!=-1)right=right+S[lc[i]];

    }

    b=i;

    }

    y=fa[head[y]];

    }

    }

    int bx,by,flg=depth[x]<depth[y];

    if (flg)bx=lc[x],by=rc[y];

    else bx=rc[x],by=lc[y];

    while (x!=y){

    if (dep2[x]>dep2[y]){

    if (flg && bx==lc[x]){

    left=left+R[x];

    if (rc[x]!=-1)left=left+S[rc[x]].reverse();

    }else if (!flg && bx==rc[x]){

    left=left+R[x];

    if (lc[x]!=-1)left=left+S[lc[x]];

    }

    bx=x;

    x=tf[x];

    }else{

    if (flg && by==rc[y]){

    right=right+R[y];

    if (lc[y]!=-1)right=right+S[lc[y]];

    }else if (!flg && by==lc[y]){

    right=right+R[y];

    if (rc[y]!=-1)right=right+S[rc[y]].reverse();

    }

    by=y;

    y=tf[y];

    }

    }

    rec ret=left+R[x]+right.reverse();

    return max(ret.b[0][0],ret.b[0][1]);

    }

    50多行,占代码中动态树部分的一半。

    这里有特别多的细节,必须把所有东西都想明白才能AC。

    效果是让人欣慰的:zjoi2011道馆之战那题

    4 121844(5) fanhqme 5648 KB 1910 MS C++ 4821 B 2011-06-18 22:29:08

    排名刷到了第4,非常有成就感。

    我造了一组数据,通过gprof的统计,全局平衡二叉树的实现调用合并统计信息的函数的次数比link-cut tree的实现少60%

    67.24      0.39     0.39  1601129     0.00     0.00  rec::operator+(rec const&) const

    78.43      0.80     0.80  4291053     0.00     0.00  rec::operator+(rec const&) const

    这就是为什么它的常数小;实际上,程序运行的大部分时间都花费在计算统计信息的和上了。

    不过,正如唐文斌曾经问过的一个经典问题,”这东西你写过不重要,重要的是你还想写第2遍吗?”

    我斩钉截铁地回答:不!

    那么,动态树的高性价比的实现是什么呢?

    树块剖分!

    http://cid-354ed8646264d3c4.office.live.com/self.aspx/.Public/DynamicTree/2325^_1.cpp

    动态树的部分就50多行(差不多跟GBT(Global  Balanced Tree)的Ask部分一样长),可以先写朴素形式,之后改动20行变成树块剖分形式。

    树块剖分的思想是什么呢?

    我们依然把树剖分,不过,这回我们是把树剖分成若干个联通块。每个联通块里,我们维护每个点到整个联通块的根(连通块中深度最小的点)的信息和。

    统计两个点之间的路径的信息的时候,我们把这个路径拆分到各个树块中,这样,除了这两个点的LCA所在的树块以外,其他的树块中的路径都是已经计算好的,直接累加即可。而LCA所在的树块中的信息可以暴力统计。

    把树块的大小设定为sqrt(N),那么算法的时间复杂度就是sqrt(N)。虽然比logN大很多,不过,实际效果非常好:

    下面是我造的一组道馆之战的数据的gprof的结果,可以看出,它的常数比link-cut tree仅仅大一点。

    73.91      0.85     0.85  4408485     0.00     0.00  rec::operator+(rec const&) const(树块剖分)

    78.43      0.80     0.80  4291053     0.00     0.00  rec::operator+(rec const&) const(link-cut tree)

    67.24      0.39     0.39  1601129     0.00     0.00  rec::operator+(rec const&) const(GBT)

    实践中效果也很好,zjoi的两个题AC毫无压力,甚至跑的比一些写的不是很好的link-cut tree还快。

    有一个问题:如何让每个联通块的大小都是sqrt(N)呢?

    当然,这是一个不可能的任务。

    不过,我们可以把要求降低一点:

    每个点到根的路径上的树块数为O(sqrt(N)),每个树块大小<=sqrt(N)。

    这样,就有了一个简单的思路:尝试合并dfs入栈序相邻的两个节点,直到块的大小满了。这个可以用另一种语言来描述:

    def dfs(a,color):

      a.belong=color

      color.size+=1

      for i in a.childs:

        if color.size<L:

            dfs(i,color)

        else:

            dfs(i,i)

    嗯,这个python程序表达的就是那个意思吧(换一种语言。。。)。

    为什么这么做是对的呢?一句话证明:路径上相邻两个树块的大小的和肯定超过L。

    划分完联通块之后,更新和查询都很好写。更新就是从这个节点往下dfs,修改同一个树块内的统计信息。查询就是两个节点比赛往上爬,直到找到LCA。

    O(sqrt(N))是一个理论上的复杂度,实际中,比较弱的数据(例如随机数据)上根本到不了这个复杂度。所以,AC起来就非常愉快。

    如何卡掉树块剖分呢?好像菊花形数据应该可以(一条链+一个星)让它达到理论复杂度

    动态树除了维护形态静止的树上的信息外,还要处理动态的问题。

    例如:

    把一个子树砍下来

    把另一棵树接上去

    改变一个树的根

    link-cut tree生下来就是为了处理这些问题的,通过强大的splay可以很容易来处理树的形态改变的问题。

    那么其他的呢?

    GBT可以见阎王了。当然,也可以有一些补救办法,例如修改的时候忽略2logN的约束,当树的形态太不像样了就重新建一遍(类似暴力懒惰删除)

    树块剖分呢?

    其实是可以在一定程度上动态起来的。

    我们用如下的方法来维护树块:

    对每次要访问的节点x,沿x走到根,把路径上相邻两个能合并的树块合并。

    这样,理论复杂度不变,均摊下来都是O(sqrt(N))。(N是整个森林的节点数)

    这么做得好处是,我们可以在树的形态改变的时候比较“奔放”地处理树块,在查询的时候再让它们规整起来就行了。

    砍树和接树都能快速完成,唯独修改根不行。

    总结一下吧。

    目前的动态树问题都是静态的树,动态的信息。

    追求代码短可以使用树块剖分,简洁高效易调错。

    追求稳定可以用link-cut tree,复杂度有保证。

    如果你的时间真的很充裕,如果你真的很牛,可以写GBT,效率高,常数小,时间复杂度低。

    未来的发展方向呢?

    1.重量级统计信息(例如6*N最短路之类的)

    2.动态的形态(例题:动态最小生成树,正在研发中。。。)

    3.和dp优化、网络流、图论等结合(例题:无向图动态连通性,正在找思路中。。。)

  • 相关阅读:
    Java中的char究竟能存中文吗?
    AOP通过反射获取自定义注解
    烂翻译系列之面向.NET开发人员的Dapr——Actors构建块
    烂翻译系列之面向.NET开发人员的Dapr——目录
    烂翻译系列之面向.NET开发人员的Dapr——前言
    烂翻译系列之面向.NET开发人员的Dapr——分布式世界
    烂翻译系列之面向.NET开发人员的Dapr——俯瞰Dapr
    烂翻译系列之面向.NET开发人员的Dapr——入门
    烂翻译系列之面向.NET开发人员的Dapr——总结和前景
    烂翻译系列之面向.NET开发人员的Dapr——机密
  • 原文地址:https://www.cnblogs.com/c4isr/p/2542914.html
Copyright © 2011-2022 走看看