[置顶] 树链剖分小节

自作多情 提交于 2020-01-20 00:38:10

前段时间学习了下树链剖分,好久没看了,今天又复习一遍,赶紧写下来,别又忘了。

我们在信息学竞赛中,有时会碰到这么一类题型,在一棵树中,修改两点之间路径上的所有边(或点)上的某个变量(如边的长度,点的权值等等),然后询问单个点(或边)或者两点之间路径上的所有点(或边)的某些性质(如边权之和,最大边最小边等等)。对于这样的题,往往容易往线段树上去靠,但是,单单是用线段树是无法维护每一条链的性质的,所以我们需要一种算法将树链分开来,使得每条链可以和线段树中的一个区间一一对应上。(当然树链剖分远远不止这些简单的应用,也不一定要和线段树有什么关系,总之就是将树链剖分开来吧)。

树链剖分有很多种剖分方法,最常用的应该就是轻重边剖分了吧(在网上大部分介绍的都是这种剖分方法),什么是轻重边剖分呢?

我们首先将树中的边分为两部分,轻边和重边,记size(U)为以U为根的子树的节点的个数,令V为U的儿子中size最大的一个(如有多个最大,只取一个),则我们说边(U,V)为重边,其余的边为轻边(如下图所示红色为重边,蓝色为轻边)。


我们将一棵树的所有边按上述方法分成轻边和重边后,我们可以得到以下几个性质:

1:若(U,V)为轻边,则size(V)<=size(U)/2。

这是显然的。

2:从根到某一点的路径上轻边的个数不会超过O(logN),(N为节点总数)。

这也是很简单,因为假设从跟root到v的路径有k条轻边,它们是 root->...->v1->...->v2->......->vk->...->v,我们设size(v)=num,显然num>=1,则由性质1,我们有size(Vk)>=2,size(Vk-1)>=4......size(v1)>=2^k,显然有2^k<=N,所以k<=log2(N)。


如果我们把一条链中的连续重边连起来,成为重链,则一条链就变成了轻边与重链交替分段的链,且段数是log(N)级别的,则我们可以讲重链放在线段树中维护,轻边可放可不放,为了方便我一般还是放,但是速度就会打一点折扣了。思路就是这么多,接下来就是具体实现了。

我们需要维护一下值:

siz[v]表示以v为根的子树的节点总数。

dep[v]表示v的深度。

son[v]表示与v在同一重链上的v的儿子节点。

fa[v]表示v的父亲节点。

top[v]表示v所在链的顶端节点。

w[v]表示节点v在线段树中的位置。

siz[],son[],fa[],dep[]可以在第一遍dfs中求出来,top[],w[]可在第二遍dfs中求出来。具体过程看代码吧。

 

struct edge
{
    int to;
    int next;
}e[maxn<<1];
int box[maxn],cnt,tot;
void init()
{
    tot=0;
    son[0]=dep[0]=0;
    memset(box,-1,sizeof(box));
    cnt=0;
}
void add(int from,int to)
{
    e[cnt].to=to;
    e[cnt].next=box[from];
    box[from]=cnt++;
}
int siz[maxn],top[maxn],son[maxn],dep[maxn],w[maxn],fa[maxn];
void dfs(int now,int pre)
{
    siz[now]=1;
    fa[now]=pre;
    son[now]=0;
    dep[now]=dep[pre]+1;
    int t,v;
    for(t=box[now];t+1;t=e[t].next)
    {
        v=e[t].to;
        if(v!=pre)
        {
            dfs(v,now);
            siz[now]+=siz[v];
            if(siz[son[now]]<siz[v])
            {
                son[now]=v;
            }
        }
    }
}
void dfs2(int now,int tp)
{
    w[now]=++tot;
    top[now]=tp;
    if(son[now])
    dfs2(son[now],top[now]);
    int t,v;
    for(t=box[now];t+1;t=e[t].next)
    {
        v=e[t].to;
        if(v!=fa[now]&&v!=son[now])
        dfs2(v,v);
    }
}

 

以上是剖分过程,关于如何在树链剖分后维护两点间路径的信息,请看这里LCA的树链剖分实现

这里需要注意的是,对于有些题要修改的权值或询问的权值在点上,有的在边上,这在剖分时虽然过程没有变,但在处理的时候是有区别的,具体不同我想在下面两道题里体现。

 

权值在边上的情况。

http://codeforces.com/problemset/problem/165/D

codeforces 165D Beard Graph

题意:给一棵树,树的每条边有一种颜色,黑色或白色,一开始所有边均为黑色,有两个操作:

操作1:将第i条边变成白色或将第i条边变成黑色。

操作2 :询问u,v两点之间仅经过黑色变的最短距离。

思路:其实这道题可以不用树链剖分,存在更高效的方法,但是一时又想不到更好的例子。

因为是一棵树,所以两点之间的路径是确定的,所以只需要判断路径中是否所有的边均为黑色边即可,全是黑边意味着没有白边,所以我们可以这么做,我们将每条边剖分放入线段树中后,初始时将所有边权设为0,对操作1,如果要将一条边改为黑色,则将线段树赋值为零,否则分值为1,然后对于操作2,我们只要看两点间路径是否权之和为0即可,若为0,返回两点间距离,否则返回0。

上代码:

 

#include <iostream>
#include <string.h>
#include <stdio.h>
#include <algorithm>
#define maxn 100010
using namespace std;
#define mid ((t[p].l+t[p].r)>>1)
#define ls (p<<1)
#define rs (ls|1)
struct tree
{
    int l,r;
    int sum;
}t[maxn<<2];
void pushup(int p)
{
    t[p].sum=t[ls].sum+t[rs].sum;
}
void build(int p,int l,int r)
{
    t[p].l=l,t[p].r=r,t[p].sum=0;
    if(l==r)
    return;
    build(ls,l,mid);
    build(rs,mid+1,r);
}
void add(int p,int x,int val)
{
    if(t[p].l==t[p].r)
    {
        t[p].sum+=val;
        return;
    }
    if(x<=mid)
    add(ls,x,val);
    else
    add(rs,x,val);
    pushup(p);
}
int query(int p,int l,int r)
{
    if(t[p].l==l&&t[p].r==r)
    {
        return t[p].sum;
    }
    if(l>mid)
    return query(rs,l,r);
    else if(r<=mid)
    return query(ls,l,r);
    else
    return query(ls,l,mid)+query(rs,mid+1,r);
}
int siz[maxn],top[maxn],son[maxn],dep[maxn],w[maxn],fa[maxn];
struct edge
{
    int to;
    int next;
}e[maxn<<1];
int box[maxn],cnt,tot;
void init()
{
    tot=0;
    son[0]=dep[0]=0;
    memset(box,-1,sizeof(box));
    cnt=0;
}
void add(int from,int to)
{
    e[cnt].to=to;
    e[cnt].next=box[from];
    box[from]=cnt++;
}
void dfs(int now,int pre)
{
    siz[now]=1;
    fa[now]=pre;
    son[now]=0;
    dep[now]=dep[pre]+1;
    int t,v;
    for(t=box[now];t+1;t=e[t].next)
    {
        v=e[t].to;
        if(v!=pre)
        {
            dfs(v,now);
            siz[now]+=siz[v];
            if(siz[son[now]]<siz[v])
            {
                son[now]=v;
            }
        }
    }
}
void dfs2(int now,int tp)
{
    w[now]=++tot;
    top[now]=tp;
    if(son[now])
    dfs2(son[now],top[now]);
    int t,v;
    for(t=box[now];t+1;t=e[t].next)
    {
        v=e[t].to;
        if(v!=fa[now]&&v!=son[now])
        dfs2(v,v);
    }
}
int solve(int a,int b)
{
    int f1=top[a],f2=top[b],dist=0;
    while(f1!=f2)
    {
        if(dep[f1]<dep[f2])
        {
            swap(f1,f2);
            swap(a,b);
        }
        dist+=w[a]-w[f1]+1;
        int tmp=query(1,w[f1],w[a]);
        if(tmp)
        return -1;
        a=fa[f1];
        f1=top[a];
    }
    if(a==b)
    return dist;//注意这里
    else
    {
        if(dep[a]>dep[b])
        swap(a,b);
        int tmp=query(1,w[son[a]],w[b]);//注意这里
        if(tmp)
        return -1;
        return dist+w[b]-w[a];
    }
}
int Edge[maxn][2];
int main()
{
    int n,q,i,a,b;
    scanf("%d",&n);
    init();
    for(i=1;i<n;i++)
    {
        scanf("%d%d",&Edge[i][0],&Edge[i][1]);
        add(Edge[i][0],Edge[i][1]);
        add(Edge[i][1],Edge[i][0]);
    }
    build(1,1,n);
    dfs(1,0);
    dfs2(1,1);
    scanf("%d",&q);
    while(q--)
    {
        int k;
        scanf("%d",&k);
        if(k==3)
        {
            scanf("%d%d",&a,&b);
            printf("%d\n",solve(a,b));
        }
        else
        {
            scanf("%d",&i);
            int tmp;
            if(dep[Edge[i][0]]>dep[Edge[i][1]])
            tmp=Edge[i][0];
            else
            tmp=Edge[i][1];
            if(k==1)
            add(1,w[tmp],-1);
            else
            add(1,w[tmp],1);
        }
    }
    return 0;
}

权值在点上的情况:

 

http://acm.hdu.edu.cn/showproblem.php?pid=3966

HDU:3966 Aragorn's Story

题意:题意很明白,给一棵树,将两点之间的路径中的所有点的权值增加或减少一个数,询问特定点当前的权值大小。

思路:思路应该很清晰了,将树剖分后放进线段树中维护。

代码如下:

 

#pragma comment(linker,"/STACK:100000000,100000000")
#include <iostream>
#include <string.h>
#include <stdio.h>
#include <algorithm>
#define maxn 50010
using namespace std;
#define mid ((t[p].l+t[p].r)>>1)
#define ls (p<<1)
#define rs (ls|1)
struct tree
{
    int l,r;
    int lazy;
}t[maxn<<2];
int siz[maxn],top[maxn],son[maxn],dep[maxn],w[maxn],fa[maxn],num[maxn],tt[maxn];
void pushdown(int p)
{
    if(t[p].lazy)
    {
        t[ls].lazy+=t[p].lazy;
        t[rs].lazy+=t[p].lazy;
        t[p].lazy=0;
    }
}
void build(int p,int l,int r)
{
    t[p].l=l,t[p].r=r,t[p].lazy=0;
    if(l==r)
    {
        t[p].lazy=num[tt[l]];
        return;
    }
    build(ls,l,mid);
    build(rs,mid+1,r);
}
void add(int p,int l,int r,int val)
{
    if(t[p].l==l&&t[p].r==r)
    {
        t[p].lazy+=val;
        return;
    }
    pushdown(p);
    if(r<=mid)
    add(ls,l,r,val);
    else if(l>mid)
    add(rs,l,r,val);
    else
    {
        add(ls,l,mid,val);
        add(rs,mid+1,r,val);
    }
}
int query(int p,int x)
{
    if(t[p].l==t[p].r)
    {
        return t[p].lazy;
    }
    pushdown(p);
    if(x>mid)
    return query(rs,x);
    else
    return query(ls,x);
}
struct edge
{
    int to;
    int next;
}e[maxn<<1];
int box[maxn],cnt,tot;
void init()
{
    tot=0;
    son[0]=dep[0]=0;
    memset(box,-1,sizeof(box));
    cnt=0;
}
void add(int from,int to)
{
    e[cnt].to=to;
    e[cnt].next=box[from];
    box[from]=cnt++;
}
void dfs(int now,int pre)
{
    siz[now]=1;
    fa[now]=pre;
    son[now]=0;
    dep[now]=dep[pre]+1;
    int t,v;
    for(t=box[now];t+1;t=e[t].next)
    {
        v=e[t].to;
        if(v!=pre)
        {
            dfs(v,now);
            siz[now]+=siz[v];
            if(siz[son[now]]<siz[v])
            {
                son[now]=v;
            }
        }
    }
}
void dfs2(int now,int tp)
{
    w[now]=++tot;
    tt[tot]=now;
    top[now]=tp;
    if(son[now])
    dfs2(son[now],top[now]);
    int t,v;
    for(t=box[now];t+1;t=e[t].next)
    {
        v=e[t].to;
        if(v!=fa[now]&&v!=son[now])
        dfs2(v,v);
    }
}
void solve(int a,int b,int val)
{
    int f1=top[a],f2=top[b];
    while(f1!=f2)
    {
        if(dep[f1]<dep[f2])
        {
            swap(f1,f2);
            swap(a,b);
        }
        add(1,w[f1],w[a],val);
        a=fa[f1];
        f1=top[a];
    }
    if(a==b)
    {
        add(1,w[a],w[a],val);//注意这里
    }
    else
    {
        if(dep[a]>dep[b])
        swap(a,b);
        add(1,w[a],w[b],val);//注意这里
    }
}
int main()
{
    freopen("dd.txt","r",stdin);
    int n,m,q,a,b,c;
    char str[2];
    while(scanf("%d%d%d",&n,&m,&q)!=EOF)
    {
        init();
        int i;
        for(i=1;i<=n;i++)
        {
            scanf("%d",&num[i]);
        }
        for(i=1;i<=m;i++)
        {
            scanf("%d%d",&a,&b);
            add(a,b);
            add(b,a);
        }
        dfs(1,0);
        dfs2(1,1);
        build(1,1,n);
        while(q--)
        {
            int node;
            scanf("%s",str);
            if(str[0]=='Q')
            {
                scanf("%d",&node);
                printf("%d\n",query(1,w[node]));
            }
            else
            {
                scanf("%d%d%d", &a,&b,&c);
                if(str[0]=='I')
                solve(a,b,c);
                else
                solve(a,b,-c);
            }
        }
    }
    return 0;
}


我已将需要注意的地方在代码中标记下来了,

 

区别就是在修改最后一条链时,也就是a,b在同一条重链中时,我们不妨设dep[a]<=dep[b],这时我们知道a是原来我们要求的v,w两点的LCA。因为我们树链剖分时,将重链放入线段树中时,事实上将点与边一一对应了,每个点对应于其父节点与其连接的边,对于根节点,可设置一个虚拟节点,把它看成根节点的父节点。这样在放入线段树中的操作就可以不变(其实还是为了实现方便)。如果权值在边上,那么我要求v,w两点间的路径时,其LCA所对应的边并不在这条路径里,所以我们要少更新一条边。如果权值在点上,则LCA显然也在v与w之间的路径中,则需要更新LCA。这就是两种题的不同点。

PS:其实树链剖分还有好多应用还有拓展,不过本弱菜还没有学得到,这里只是将最基本的应用总结出来,希望各位神牛不要BS。

PS2:DFS写法容易爆栈,所以还有非递归写法,如BFS写法和模拟栈等等,不过我还没研究出来。。。

 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!