P3384 【模板】轻重链剖分

本文详细解析了树链剖分的概念,包括DFS序、重儿子、轻儿子、重边和轻边等,并介绍了如何利用DFS序将重边轻边串联形成连续链。通过树链剖分,可以将树转化为区间修改问题,简化了两个节点间路径上点权的修改操作,降低了复杂度至O(logn)。
摘要由CSDN通过智能技术生成

题目链接

直接贴模板 带注释

  • DFS序:一颗子树在DFS序上是连续的一段 。
    那么我们在进行一些操作像什么:将以某节点为根的子树节点都加上x时,就可以转化为在线段树内的区间加了。

树链剖分的一些概念:

  • 重儿子(节点):子树结点数目最多的孩子结点
  • 轻儿子(节点):除了重儿子以外的孩子结点
  • 重边:重儿子和父节点连的边
  • 轻边:轻儿子和父节点连的边
  • 重链:重边连成的链
  • 轻链:轻边连成的链

在这里插入图片描述

如图,被圈起来的就是重儿子,红色的边就是重边。

在DFS过程中,可以得到每个节点深度,父节点,重儿子,子树的节点个数。

  • 从根到某一点的路径上,不超过logn条轻边和不超过logn条重边。

得到了重儿子,轻儿子,我们需要把重边轻边串成链。即:让重儿子在区间上是连续的。

这里我们就要用到DFS序了:从根开始dfs,打上dfs序,记录链顶元素,优先走重儿子。走轻儿子的时候把轻儿子设为其链顶元素。

void dfs2(int rt,int t)//t是rt所在链的链顶
{
	pos[++cnt]=rt;//dfs序数组
	in[rt]=cnt;
	top[rt]=t;//链顶
	if(son[rt])//重儿子存在
	{
		dfs2(son[rt],t);
	}
	for(int i=head[rt];i;i=e[i].next)
	{
		int to=e[i].to;
		if(to == fa[rt] || to == son[rt]) continue;
		dfs2(to,to);
	}
}

这样得到了每条链在dfs序上都是连续的,如果我们需要修改树上两个顶点最短路径上的点,那么就可以转化成区间修改了。

比如我们要修改x y最短路径上的点权:

  • 如果x y位于同一条链(设in[x]<in[y]),那么直接区间修改[in[x],in[y]]
  • 否则,设x为链顶更深的那个节点,fx为其链顶,先区间修改[in[fx],in[x]],再把x更新为x=fa[fx],然后fx=top[x]
  • 循环第二步,直到x y链顶相同,执行第一步
inline void updata_chain(int x,int y,int z)//x y最短路径上的点 点权+z
{
    int fx=top[x],fy=top[y];
    while(fx != fy) 
    {
        if(dep[fx] < dep[fy]) swap(x,y),swap(fx,fy);//保证fx是链顶最深的点的链顶
        //此时得到一条连续的链 fx->x  修改这个区间
        updata(1,1,n,in[fx],in[x],z);
        x=fa[fx];//fx->x这条链改完了 x变成链顶的父节点 继续修改
        fx=top[x];
    }
    //此时x y在同一条链当中 (深度小的那个点就是原先x、y的LCA 且x,y之间的最短距离就是d[x]+d[y]-2*d[LCA] (d[i]是i到根节点的距离))
    if(in[x]>in[y]) swap(x,y);
    updata(1,1,n,in[x],in[y],z);
    return ;
}

树链剖分本质上就是把树剖分成了若干条不相交的链,每条链对应dfs序上一段连续的区间。于是在维护两个点路径上的点权的时候,就不需要单点修改,而是变成了log次区间修改。

#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int maxn = 1e5+7;
const ll inf = 34359738370;
int n,m,r,p;
struct 
{
    int to,next;
}e[maxn<<1];
int head[maxn],num=1;
void add(int v,int u)
{
    e[num].to=u;
    e[num].next=head[v];
    head[v]=num++;
}
int son[maxn],size[maxn],fa[maxn],dep[maxn];
//  重孩子    树的节点个数  父节点   节点深度   
void dfs1(int rt,int par)
{
    size[rt]=1;
    for(int i=head[rt];i;i=e[i].next)
    {
        int to=e[i].to;
        if(to == par) continue;
        fa[to]=rt;
        dep[to]=dep[rt]+1;
        dfs1(to,rt);
        size[rt]+=size[to];
        if(size[to]>size[son[rt]]) son[rt]=to;
    }
}
int top[maxn],pos[maxn],w[maxn],in[maxn],cnt=0;
//  所在链的顶端 dfs序数组 原本的树节点的点权 dfs序的时间戳 时间戳标记
//in[i] 节点i的dfs序  pos[i] dfs序中第i个元素的节点编号
//i-> w[pos[i]] 对这个数组建线段树 维护区间和
void dfs2(int rt,int t)//t是rt所在链的链顶
{
    pos[++cnt]=rt;
    in[rt]=cnt;
    top[rt]=t;
    if(son[rt]) dfs2(son[rt],t);//优先访问重链 保证重链节点都在dfs序的一段连续区间内
    for(int i=head[rt];i;i=e[i].next)
    {
        int to=e[i].to;
        if(to == fa[rt] || to==son[rt]) continue;
        dfs2(to,to);//访问轻链的时候自己(非重孩子)就是轻链的顶端  重孩子按定义不可以是链顶
    }
}
//树链刨分
int tree[maxn<<2],tag[maxn<<2];//对dfs序建线段树  维护dfs序数组的区间和
inline int lc(int &rt)
{
    return rt<<1;
}
inline int rc(int &rt)
{
    return rt<<1|1;
}
inline void pushup(int rt)
{
    tree[rt]=(tree[lc(rt)]+tree[rc(rt)])%p;
}
inline void change(int rt,int l,int r,int v)//区间[l,r] +v
{
    tree[rt]+=(r-l+1)*v;
    tag[rt]+=v;
}
inline void build(int rt,int l,int r)
{
    if(l == r)
    {
        tree[rt]=w[pos[l]];
        return ;
    }
    int mid=(l+r)>>1;
    build(lc(rt),l,mid);
    build(rc(rt),mid+1,r);
    pushup(rt);
}
inline void pushdown(int rt,int l,int r)
{
    if(tag[rt])
    {
        int mid=(l+r)>>1;
        change(lc(rt),l,mid,tag[rt]);
        change(rc(rt),mid+1,r,tag[rt]);
        tag[rt]=0;
    }
    return ;
}
inline void updata(int rt,int l,int r,int vl,int vr,int v)//区间加v
{
    if(l>vr || r<vl) return ;
    if(vl<=l && r<=vr) 
    {
        change(rt,l,r,v);
        return ;
    }
    int mid=(l+r)>>1;
    pushdown(rt,l,r);
    updata(lc(rt),l,mid,vl,vr,v);
    updata(rc(rt),mid+1,r,vl,vr,v);
    pushup(rt);
}
inline int query(int rt,int l,int r,int vl,int vr)
{
    if(l>vr || r<vl) return 0;
    if(vl<=l && r<=vr) 
    {
        return tree[rt];
    }
    int mid=(l+r)>>1;
    pushdown(rt,l,r);
    return (query(lc(rt),l,mid,vl,vr)+query(rc(rt),mid+1,r,vl,vr))%p;
}
//任意两点之间路上 只能有log条轻链+log条重链  
//求LCA过程就是 把链顶深度大的这个点往上跳到链顶 如果还不在同一链就继续循环该过程 直到2个点在同一个链中,深度小的就是LCA
inline void updata_chain(int x,int y,int z)//x y最短路径上的点 点权+z
{
    int fx=top[x],fy=top[y];
    while(fx != fy) 
    {
        if(dep[fx] < dep[fy]) swap(x,y),swap(fx,fy);//保证fx是链顶最深的点的链顶
        updata(1,1,n,in[fx],in[x],z);
        x=fa[fx];//fx->x这条链改完了 x变成链顶的父节点 继续修改
        fx=top[x];
    }
    //此时x y在同一条链当中 (深度小的那个点就是原先x、y的LCA 且x,y之间的最短距离就是d[x]+d[y]-2*d[LCA] (d[i]是i到根节点的距离))
    if(in[x]>in[y]) swap(x,y);
    updata(1,1,n,in[x],in[y],z);
    return ;
}
inline int query_chain(int x,int y)
{
    int fx=top[x],fy=top[y],ans=0;
    while(fx != fy)
    {
        if(dep[fx] < dep[fy]) swap(x,y),swap(fx,fy);//保证fx是最深点的链顶
        ans=(ans+query(1,1,n,in[fx],in[x]))%p;
        x=fa[fx];//fx->x这条链查询 x变成链顶的父节点 继续查询
        fx=top[x];
    }
    if(in[x] > in[y]) swap(x,y);
    ans=(ans+query(1,1,n,in[x],in[y]))%p;
    return ans;
}
int main()
{
    scanf("%d %d %d %d",&n,&m,&r,&p);
    for(int i=1;i<=n;i++) scanf("%d",w+i);
    for(int i=1;i<n;i++)
    {
        int v,u;
        scanf("%d %d",&v,&u);
        add(v,u),add(u,v);
    }
    dfs1(r,r);//求出重孩子
    dfs2(r,r);//重链划分+求dfs序
    build(1,1,n);
    while(m--)
    {
        int f,x,y,z;
        scanf("%d",&f);
        if(f == 1)//x y链上都加z
        {
            scanf("%d %d %d",&x,&y,&z);
            updata_chain(x,y,z);
        }
        else if(f == 2)//查询x y最短路径上的节点点权和
        {
            scanf("%d %d",&x,&y);
            printf("%d\n",query_chain(x,y));
        }
        else if(f == 3)//修改以x为根的整颗子树
        {
            scanf("%d %d",&x,&z);
            updata(1,1,n,in[x],in[x]+size[x]-1,z);
        }
        else //查询以x为根的整颗子树
        {
            scanf("%d",&x);
            printf("%d\n",query(1,1,n,in[x],in[x]+size[x]-1));
        }
    }
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值