树链剖分详解

前置概念:

  • size[i]表示节点i的子树大小,其中子树大小定义为包括该点自身的子树含有的节点数个数。

  • 轻儿子和重儿子:一个节点儿子中size最大的叫做重儿子,剩下的都叫轻儿子

  • 轻重边:连接x及其重儿子的边叫做重边,同理连接x及其轻儿子的边叫做轻边

  • 轻重链:多条重边形成的链叫做重链,且规定每个节点都属于某一条重链。

  • top[i]表示i节点所在重链的顶端的点的编号

  • f[i]表示i节点的父节点编号

  • son[i]表示i节点的重儿子编号,编号为0代表无重儿子

  • d[i]表示i节点的深度,规定根节点深度为1

前置基础信息的求解:

  • 第一次dfs可以求出,d[i],f[i],son[i],size[i]

供参考具体实现:

void dfs1(int x,int fa)
{
    size[x]=1;
    d[x]=d[fa]+1;
    son[x]=0;
    f[x]=fa;//初始化
    for(int i=head[x];~i;i=edge[i].next){
        int to=edge[i].to;
        if(to==fa)continue;
        dfs1(to,x);
        size[x]+=size[to];
        if(size[son[x]]<size[to]) son[x]=to;
    }//遍历子节点,更新信息
}
  • 第二次dfs借助son[i]等信息求出top[i]。

具体实现:

void dfs2(int x,int topx)//topx记录重链顶端的点
{
    top[x]=x;
    if(son[x]!=0)dfs2(son[x],topx);
    for(int i=head[x];~i;i=edge[i].next)
    if(edge[i].to!=son[x]&&edge[i].to!=f[x])
    dfs2(edge[i].to,edge[i].to);//遍历轻边,并为其创建新的重链
}

引申:

最近公共祖先(LCA):

  1. 概念:

u与v的LCA就是u到v的路径上深度最小的点。因为在一棵树中只有可能有一条路径能从一个点到另一个点(如果两点之间有两条路径,那么在无向图中会构成自环,从而违背了树的定义)则LCA有且仅有固定的一个点。

  1. 求解LCA算法:

  1. 给定两点u和v,如果两点在同一条重链中,lca即为两点中深度较小的点;

  2. 若不在,其重链顶端点中较深的点x跳至其f[top[x]]上,每次操作会跳过一条轻边,再来观察两点是否在同一条重链中,重复操作。

代码实现:

void lca(int u,int v)
{
    while(top[u]!=top[v])
    {
        if(d[top[u]]<d[top[v]])v=f[top[v]];//深度越大代表点靠近底部
        else u=f[top[u]];
    }
    printf("%d\n",d[u]<d[v]?u:v);
}

算法时间复杂度分析:

首先最坏情况下,一个点可能不经过任意一条重链只跳轻边达到根节点,那么操作次数就与需跳跃的轻边数量有关,每一次通过轻儿子跳轻边来到父节点,父节点的size一定大于等于2*size{slimson},假设从size=1的轻儿子节点处开始跳,那么跳跃的轻边数量一定小于等于log_2n,则实际的时间复杂度应该为O(logn),是非常小的数字。

树上操作(树链剖分加线段树):

树上操作复杂度较高,考虑线段树维护该有根多边树上的节点信息

  • 编号,根据第二次dfs进行编号,使得重链上的点新编号是连续的,每个点的子树编号也在一个连续区间内。

  • int dfn=0,nid[maxn],oid[maxn];//nid[x]的值为原来点的新编号,oid[x]的值表示新编号的对应的原来的点编号
    void dfs2(int x,int topx)
    {
        top[x]=topx;
        oid[++dfn]=x;
        nid[x]=dfn;//编新号,重链一堆,子树一堆
        na[dfn]=a[x];//对应的权值顺序也要调整
        if(son[x]!=0)dfs2(son[x],topx);
        for(int i=head[x];~i;i=edge[i].next)
        if(edge[i].to!=son[x]&&edge[i].to!=f[x])
        dfs2(edge[i].to,edge[i].to);
    }

  • 树上操作

  1. 修改(或其它操作)以某个节点为根节点的子树上所有点的权值,即对应修改线段树[nid[x],nid[x]+size[x]-1]区间范围内的值。

  2. 修改某个点或边的权值(边的权值可存储到点中),对应线段树的单点更新

  3. 修改某条重链上两点间路径的的所有边权,相当于修改线段树区间[nid[x]+1,nid[y]](前提:x点的深度较小,对应编号较小)

  4. 修改不在同一重链的两点间的边权或点权;首先,判断dep[top[u]]与dep[top[v]]大小,先跳转深度较小的点,同时修改x到f[top[x]]这条重链之间所有的边权;直到跳转至两点处于同一条重链中,修改两点之间的边权。

代码实现:

#include<bits/stdc++.h>
#define ll long long
#define INF 0x3f3f3f3f
#define maxn 1000
using namespace std;
int n,m,r,P,cnt=0;
ll a[maxn]={0},na[maxn]={0};
int head[maxn]={0},d[maxn]={0},f[maxn]={0},top[maxn]={0},son[maxn]={0},size[maxn]={0};
struct Edge{
    int to;
    int next;
}edge[maxn<<1];
struct node{
    ll val;
    ll lazy;
}segtree[maxn<<2];
int dfn=0,nid[maxn],oid[maxn];//nid[x]的值为原来点的新编号,oid[x]的值表示新编号的对应的原来的点编号
void addedge(int u,int v)
{
    edge[++cnt].to=v;
    edge[cnt].next=head[u];
    head[u]=cnt;
}
void dfs1(int x,int fa)
{
    d[x]=d[fa]+1;
    f[x]=fa;
    son[x]=0;
    size[x]=1;
    for(int i=head[x];~i;i=edge[i].next)
    {
        int to =edge[i].to;
        if(to==fa)continue;
        dfs1(to,x);
        size[x]+=size[to];
        if(size[son[x]]<size[to])son[x]=to;
    }
}
void dfs2(int x,int topx)
{
    top[x]=topx;
    oid[++dfn]=x;
    nid[x]=dfn;//编新号,重链一堆,子树一堆
    na[dfn]=a[x];
    if(son[x]!=0)dfs2(son[x],topx);
    for(int i=head[x];~i;i=edge[i].next)
    if(edge[i].to!=son[x]&&edge[i].to!=f[x])
    dfs2(edge[i].to,edge[i].to);
}
//以下线段树
void pushup(int rt)
{
    segtree[rt].val=(segtree[rt*2].val+segtree[rt*2+1].val)%P;
}
void pushdown(int rt,int ln,int rn)
{
    if(segtree[rt].lazy)
    {
        int l=rt*2,r=rt*2+1,lazy=segtree[rt].lazy;
        segtree[l].val=(lazy*ln+segtree[l].val)%P;
        segtree[l].lazy=(lazy+segtree[l].lazy)%P;
        segtree[r].val=(lazy*rn+segtree[r].val)%P;
        segtree[r].lazy=(lazy+segtree[r].lazy)%P;
        segtree[rt].lazy=0;//不要忘!!!!!!!!!!!!!!
    }
}
void build(int l,int r,int rt)
{
    segtree[rt].lazy=0;
    if(l==r)
    {
        segtree[rt].val=na[l]%P;//rt指的是segtree的节点编号,l,r指的是原数组的区间编号
        return;
    }
    int mid=(r+l)>>1;
    build(l,mid,rt*2);
    build(mid+1,r,rt*2+1);
    pushup(rt);
}
void updated(int L,int R,int l,int r,int rt,int val)//L,R代表修改区间!!!!//区间更新
{
    if(l>=L&&r<=R)
    {
        segtree[rt].val=(segtree[rt].val+(r-l+1)*val)%P;
        segtree[rt].lazy=(val+segtree[rt].lazy)%P;
        return;
    }
    int mid=(l+r)>>1;
    pushdown(rt,mid-l+1,r-mid);
    if(mid>=L)
    updated(L,R,l,mid,rt*2,val);
    if(mid<R)//必须是小于,没有等于
    updated(L,R,mid+1,r,rt*2+1,val);
    pushup(rt);
}
ll query(int L,int R,int l,int r,int rt)//区间查询
{
    if(l>=L&&r<=R)
    return segtree[rt].val%P;
    if(l>R||r<L)
    return 0;
    int mid=(l+r)>>1;
    pushdown(rt,mid-l+1,r-mid);
    return (query(L,R,l,mid,rt*2)%P+query(L,R,mid+1,r,rt*2+1)%P)%P;
}
void update_rd(int u,int v,int val)//路径更新
{
    while(top[u]!=top[v])
    {
        if(d[top[u]]>d[top[v]]){
            updated(nid[top[u]],nid[u],1,n,1,val);
            u=f[top[u]];
        }
        else {
            updated(nid[top[v]],nid[v],1,n,1,val);
            v=f[top[v]];
        }
    }
    if(nid[u]>nid[v])
    updated(nid[v],nid[u],1,n,1,val);
    else
    updated(nid[u],nid[v],1,n,1,val);
}
ll query_rd(int u,int v)//路径查询
{
    ll res=0;
    while(top[u]!=top[v])
    {
        if(d[top[u]]<d[top[v]])
        {
            res=(res+query(nid[top[v]],nid[v],1,n,1))%P;
            v=f[top[v]];
        }
        else
        {
            res=(res+query(nid[top[u]],nid[u],1,n,1))%P;
            u=f[top[u]];
            
        }
    }
    if(nid[u]>nid[v])
    res=(res+query(nid[v],nid[u],1,n,1))%P;
    else
    res=(res+query(nid[u],nid[v],1,n,1))%P;
    return (res%P);
}
int main()
{
    scanf("%d%d%d%d",&n,&m,&r,&P);
    for(int i=1;i<=n;i++)
    scanf("%lld",&a[i]);
    for(int i=0;i<=n;i++)
    head[i]=-1;
    for(int i=1;i<=n-1;i++)
    {
        int u,v;
        scanf("%d%d",&u,&v);
        addedge(u,v);
        addedge(v,u);
    }
    dfs1(r,r);
    dfs2(r,r);
    build(1,n,1);//原根新编号为1
    for(int i=1;i<=m;i++)
    {
        int x,y,z,mod;
        scanf("%d",&mod);
        if(mod==1)
        {
            scanf("%d%d%d",&x,&y,&z);
            update_rd(x,y,z);
        }
        else if(mod==2)
        {
            scanf("%d%d",&x,&y);
            printf("%lld\n",query_rd(x,y));
        }
        else if(mod==3)
        {
            scanf("%d%d",&x,&z);
            updated(nid[x],nid[x]+size[x]-1,1,n,1,z);
        }
        else
        {
            scanf("%d",&x);
            printf("%lld\n",query(nid[x],nid[x]+size[x]-1,1,n,1));
        }
    }
    system("pause");
}//注意事项:1.细节细节细节!不要看错,漏写2.注意该模余数的地方就去模3.最好能熟悉线段树的所有细节,以及将原来的点赋予新编号时,一定要小心谨慎
//4.码量巨大,稳住心态

  • 23
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值