树链剖分学习笔记

 毕竟刚学到了皮毛,还不足以讲,如果想较为深入的学习的话,请观摩博客:https://www.cnblogs.com/zwfymqz/p/8094500.html

更博啦。经过这几天的树链剖分学习,感觉还不是那么难,所以就写一下笔记,供大家分享。这份博客就用洛谷的一道模板题来讲https://www.luogu.org/problemnew/show/P3384

学习之前必须要掌握一些知识,不然你看几天都不会看得懂:线段树,dfs序。

如果掌握了就来看一下这几个定义

  • 重结点:子树结点数目最多的结点;
  • 轻节点:父亲节点中除了重结点以外的结点;
  • 重边:父亲结点和重结点连成的边;
  • 轻边:父亲节点和轻节点连成的边;
  • 重链:由多条重边连接而成的路径;
  • 轻链:由多条轻边连接而成的路径;

定义没有什么好说的,我们就开始正式讲树链剖分。

树链剖分

比如上面这幅图中,用黑线连接的结点都是重结点,其余均是轻结点,2-11、1-11就是重链,其他就是轻链,用红点标记的就是该结点所在链的起点,也就是我们👇提到的top结点,还有每条边的值其实是进行dfs时的执行序号。

然后就是实现这个算法的辅助数组

名称解释
size[x]     保存以x为根的子树节点个数
top[x] 保存当前节点所在链的顶端节点
son[x] 保存重儿子
deep[x]        保存结点x的深度值
fa[x] 保存结点u的父亲节点
id[x] 保存树中每个节点剖分以后的新编号(DFS的执行顺序)

 

 

 

 

 

 

 

 

 

 

此外还有两条性质

1.如果(u,v)是轻边,那么size[v]<size[u]/2

2.从根结点到任意结点的路所经过的轻重链的个数必定都小与O(logn);

然后我们要对这棵树进行预处理

我们用到了两个dfs,dfs1是处理子树大小和父子关系还有节点深度的,dfs2是用来处理编号和重链以及中儿子的。

然后这两个dfs比较好理解,这里就只粘代码,代码里面有注释,可以感性理解一下。

void dfs1(int x,int f)
{
    fa[x]=f;//首先记录父亲 
    size[x]=1;
    for(int i=head[x]; i!=-1; i=e[i].next)
    {
        if(!deep[e[i].to] && e[i].to != f)
        {
            deep[e[i].to]=deep[x]+1;//深度 
            dfs1(e[i].to,x);
            size[x]+=size[e[i].to];//子树大小 
            if(size[e[i].to]>size[son[x]]) son[x]=e[i].to;//如果当前子树比中儿子的子树大,那么当前子树的根为重儿子 
        }
    }
}
dfs1
void dfs2(int x,int topf)//topf为当前重链的顶部 
{
    idx[x] = ++cnt;//编号 
    top[x] = topf;
    if(!son[x]) return;//小优化 
    dfs2(son[x],topf);
    for(int i=head[x]; i!=-1; i=e[i].next)
        if(!idx[e[i].to] && e[i].to!=fa[x]) dfs2(e[i].to,e[i].to);
}
dfs2

理解了两个dfs,我们就好进行了!

其余的事情就是线段树做的事了,区间加和,区间查询,pushdown操作一样。但是树链剖分还有几个其他的操作。

  1. 节点路径修改
  2. 节点路径查询和
  3. 子树修改
  4. 子树查询
  5. 换根

节点路径修改这个操作需要用到一个函数:树上修改(change_tree)

这个思想就是用到了倍增,这样考虑,因为两个节点分别在两条重链上,然后你不断缩短重链,期间不断进行区间修改,然后这两个节点最后一定在一个重链上,在进行最后一步区间修改,大功告成。树上查询也是这样的。然后就是这段代码。

void add_tree(int x,int y,int val)
{
    while(top[x]!=top[y])
    {
        if(deep[top[x]]<deep[top[y]]) swap(x,y);
        add_interval(1,idx[top[x]],idx[x],val);
        x=fa[top[x]];
    }
    if(deep[x]<deep[y]) swap(x,y);
    add_interval(1,idx[y],idx[x],val);
}
树上修改
int asksum_tree(int x,int y)
{
    int ans=0;
    while(top[x]!=top[y])
    {
        if(deep[top[x]]<deep[top[y]]) swap(x,y);
        ans=(ans+asksum_interval(1,idx[top[x]],idx[x]))%p;
        x=fa[top[x]];
    }
    if(deep[x]<deep[y]) swap(x,y);
    return (ans+asksum_interval(1,idx[y],idx[x]))%p;
}
树上查询

子树修改和子树查询也非常的简单。你可以观察一下发现,因为树映射到线段树上的是一段区间,所以它的子树的区间为[id[x],id[x]+size[x]-1],所以代码如下

//子树修改
change_interval(1,id[x],id[x]+size[x]-1,val)
//子树查询
ask_interval(1,id[x],id[x]+size[x]-1)
子树相关

 之后就是我所说的换根操作。这个操作看似很麻烦,其实不然,我们可以用分类讨论的思想:设查询点为a,当前根节点为root

  1. 查询点与根节点相同:直接返回根节点的值
  2. LCA(a,root)!=a 那么直接返回a的子树的信息
  3. LCA(a,root)==a ,所以a和root在一棵子树,因为这棵树对应在线段树上的区间是连续的,所以我们访问除root子树之外的所有的区间,就是查询a的子树[1,id[root]-1]&[id[root]+size[x],n]

有些题有可能你会碰上树剖求LCA,这里就奉上树剖求LCA代码,代码很简单,你一定能看懂,而且你会发现耗时惊的一批

int LCA(int x,int y)
{
    while(top[x]!=top[y])
    {
        if(deep[top[x]]<deep[top[y]])swap(x,y);
        x=fa[top[x]];
    }
    return deep[x]<=deep[y]? x:y;
}
树剖求LCA

 

posted @ 2018-08-07 18:40  _Lancy  阅读(312)  评论(1编辑  收藏  举报