一、算法简介

树链剖分是一种用于树上路径修改与求和的一种算法,

其核心原理是将链分为轻重,并通过轻重链的跳跃实现简便处理。

二、术语解释

1、重(轻)子节点:

子树大小最大的子节点被称为重子节点,除了重儿子以外的子节点均为轻子节点。

2、重(轻)链:

重子节点相连形成的链被称为重链,轻子节点对应的链则为轻链。

重链上的dfs序连续。

3、链顶点:

一条链中深度最浅的点被称为链顶点,链顶点可以用于整条链的标识

tips:重轻链的概念是相对的,每一条轻链作为重链时链顶点均为本身

三、具体实现

可以通过两次dfs实现:

第一次处理出重子节点等节点间关系的内容,

第二次处理出DFS序和链相关的内容,并处理出链上内容

在查询LCA(最近公共祖先)时,

若两点不在同一条链上(链顶点不同),

则将链顶点深度较深的点往上一条链跳,

直到两点在同一条链上。

由于dfs序的连续性,

可以直接在线性数据结构上维护(如线段树、树状数组等)。

四、用处

可用于快速求LCA并处理两点简单距离,

进行查询和修改。

模板题P3384

五、标准程序

线段树版本:

#include<iostream>
#include<vector>
using namespace std;
const int MAXN=100005;
int a,b,c,modd,tot=0,t=0;
int num[MAXN],lct[MAXN],tag[MAXN<<2],dep[MAXN<<2],node[MAXN<<2];
//num点权 lctDFS序上点权 tag延迟标记 dep线段树宽度 node线段树点权
int fa[MAXN],dpt[MAXN],top[MAXN];
//fa父节点 dpt点深度 top链顶点
int hson[MAXN],dfn[MAXN],size_[MAXN];
//hson重子节点 dfnDFS序编号 size_子树宽度
struct edges
{
	int ends,to;
};
edges edge[MAXN<<1];
int head[MAXN];
inline char bit()
{
    static char buf[1000005],*p1=buf,*p2=buf;
    return p1==p2&&(p2=(p1=buf)+fread(buf,1,100005,stdin),p1==p2)?EOF:*p1++;
}//文件读入流,相当于getchar()
inline int read()
{
	char x=bit();int t=0;
	while(!isdigit(x))x=bit();
	while(isdigit(x))t=(t<<3)+(t<<1)+x-'0',x=bit();
	return t;
}//快速读入
inline void init(int x)
{
	size_[x]=1,hson[x]=0;
	//如果是叶子节点也有自己本身的深度1,重子节点开始先初始化为0
	for(register int i=head[x];i;i=edge[i].to)
	{
		int now=edge[i].ends;
		if(now==fa[x])continue;
		fa[now]=x;//父节点处理出来防止死循环
		dpt[now]=dpt[x]+1;//深度先处理出来
		init(now);
		size_[x]+=size_[now];//扫完子树后更新子树大小
		if(size_[now]>size_[hson[x]])hson[x]=now;
		//更新真正子树最大的节点,即重子节点
	}
}
inline void build(int x,int k)
{
	dfn[x]=++tot,lct[tot]=num[x];
	//处理DFS序,并把点权映射到DFS序上
	top[x]=k;//处理出顶点
	if(hson[x])build(hson[x],k);
	//优先扫重子节点,保证重链DFN序的连续性
	for(register int i=head[x];i;i=edge[i].to)
	{
		int now=edge[i].ends;
		if(now==fa[x]||now==hson[x])continue;
		build(now,now);
		//轻子节点的链顶点为自己本身
	}
}
inline void binary_tree(int k,int l,int r)
{
	dep[k]=r-l+1;
	if(l==r)
	{
		node[k]=lct[l];
		return;
	}
	int mid=(l+r)>>1;
	binary_tree(k<<1,l,mid);
	binary_tree(k<<1|1,mid+1,r);
	(node[k]=node[k<<1]+node[k<<1|1])%=modd;
}//预处理线段树
inline void update(int k)
{
	(node[k<<1]+=dep[k<<1]*tag[k])%=modd;
	(node[k<<1|1]+=dep[k<<1|1]*tag[k])%=modd;
	(tag[k<<1]+=tag[k])%=modd;
	(tag[k<<1|1]+=tag[k])%=modd;
	tag[k]=0;
}//下传延迟标记
/*注意!延迟标记维护的点已经更新过了,所以在下传的时候不需要更新自身*/
inline void add(int k,int l,int r,int x,int y,int z)
{
	if(x<=l&&r<=y)
	{
		node[k]+=z*dep[k];
		tag[k]+=z;
		//如果包含在查询范围内,可以直接更新
		return;
	}
	update(k);
	int mid=(l+r)>>1;
	if(x<=mid)add(k<<1,l,mid,x,y,z);
	if(y>mid)add(k<<1|1,mid+1,r,x,y,z);
	node[k]=node[k<<1]+node[k<<1|1];
}//线段树更新
inline int findx(int k,int l,int r,int x,int y)
{
	if(x<=l&&r<=y)return node[k]%modd;
	update(k);
	int mid=(l+r)>>1,query=0;
	if(x<=mid)(query+=findx(k<<1,l,mid,x,y))%=modd;
	if(y>mid)(query+=findx(k<<1|1,mid+1,r,x,y))%=modd;
	return (query%=modd);
}//线段树查询
inline void change_line(int x,int y,int z)
{
	z%=modd;
	while(top[x]!=top[y])
	{
		if(dpt[top[x]]<dpt[top[y]])x^=y,y^=x,x^=y;
		add(1,1,a,dfn[top[x]],dfn[x],z);
		x=fa[top[x]];
		//保证链顶节点深度深的向上传,因为点是跳到链顶节点的父节点的
	}
	if(dpt[x]>dpt[y])x^=y,y^=x,x^=y;
	add(1,1,a,dfn[x],dfn[y],z);
}//修改两点简单路径
inline int find_line(int x,int y)
{
	int query=0,memory;
	while(top[x]!=top[y])
	{
		if(dpt[top[x]]<dpt[top[y]])x^=y,y^=x,x^=y;
		memory=findx(1,1,a,dfn[top[x]],dfn[x]);
		(query+=memory)%=modd;
		x=fa[top[x]];
		//整条链可以直接处理
	}
	if(dpt[x]>dpt[y])x^=y,y^=x,x^=y;
	memory=findx(1,1,a,dfn[x],dfn[y]);
	query+=memory;
	return query%modd;
}
int main()
{
	a=read();b=read();c=read();modd=read();
	for(register int i=1;i<=a;++i)num[i]=read();
	for(register int i=1;i<a;++i)
	{
		int x,y;
		x=read();y=read();
		edge[++t]={y,head[x]},head[x]=t;
		edge[++t]={x,head[y]},head[y]=t;
	}
	dpt[c]=1;
	init(c);build(c,c);
	binary_tree(1,1,a);
	for(register int i=1;i<=b;++i)
	{
		int opt,x,y,z,res=0;
		opt=read();x=read();
		switch(opt){
		case 1:
			y=read();z=read();
			change_line(x,y,z);
			break;
		case 2:
			y=read();
			printf("%d\n",find_line(x,y)%modd);
			break;
		case 3:
			z=read();
			add(1,1,a,dfn[x],dfn[x]+size_[x]-1,z);
			break;
		case 4:
			printf("%d\n",(findx(1,1,a,dfn[x],dfn[x]+size_[x]-1))%modd);
			break;
		}
	}
	return 0;
}

树状数组版本

#include<iostream>
#define swap(x,y) x^=y,y^=x,x^=y;
#define ll long long
#define lowbit(x) x&(-x)
//补码的形式为取反
using namespace std;
const int MAXN=100005;
int a,b,c,modd,tot=0,cnt=0;
//tot记dfn,cnt记链式前向星
int fa[MAXN],size_[MAXN],dpt[MAXN];
//fa父节点,size_子树大小,dpt结点深度
int dfn[MAXN],top[MAXN],hson[MAXN];
//dfnDFS序,top链顶节点,hson重儿子
int lct[MAXN],head[MAXN];
//lctDFS序上点权,head链式前向星用
ll num[MAXN],node[MAXN],sum[MAXN];
//num点权,nodeDFS序上树状数组点权,sum同node,用于计算区间和
/*如果不知道如何树状数组算区间和可以去网上搜搜
  思路是推出公式ans=(x+1)*Σnum[i]-Σsum[i] */
struct edges
{
    int ends,to;
};edges edge[MAXN<<1];
//链式前向星
inline char bit(){
    static char buf[1000005],*p1=buf,*p2=buf;
    return p1==p2&&(p2=(p1=buf)+fread(buf,1,1000000,stdin),p1==p2)?EOF:*p1++;
}//文件流读入,用于替换getchar函数加速
inline int read(){
    //#define bit() getchar()
    //把define前注释去掉即可手动输入
    char x=bit();int t=0;
    while(!isdigit(x))x=bit();
    while(isdigit(x))t=(t<<3)+(t<<1)+x-'0',x=bit();
    return t;
}//快速读入
void dfs1(int x)
{
    size_[x]=1,hson[x]=0;
    for(int i=head[x];i;i=edge[i].to)
    {
        int now=edge[i].ends;
        if(fa[x]==now)continue;
        //防止回传导致DFS卡死
        fa[now]=x;
        dpt[now]=dpt[x]+1;
        //预处理父节点和深度
        dfs1(now);
        size_[x]+=size_[now];
        //处理完子树后可得子树大小
        hson[x]=size_[now]>size_[hson[x]]?now:hson[x];
        //重儿子定义为子树大小最大的儿子
        /*以上表达方式为三目运算符,?前为条件
        结果为true则返回:前的值,否则返回:后的值*/
    }
}//第一次DFS,用于寻找重儿子及处理其他信息
void dfs2(int x,int y)
{
    dfn[x]=++tot,lct[tot]=num[x],top[x]=y;
    if(hson[x])dfs2(hson[x],y);
    //重儿子连在原来的链上
    for(int i=head[x];i;i=edge[i].to)
    {
        int now=edge[i].ends;
        if(now==fa[x]||now==hson[x])continue;
        dfs2(now,now);
        /*轻儿子为所有非重儿子的儿子,
        则其自身可构成另一条链,
        且链顶点为自身*/
    }
}//第二次DFS,用于处理DFS序及重链,并讲
inline void add(int x,int query){
    for(register int i=x;i<=a;i+=lowbit(i))
    node[i]+=(ll)query,sum[i]+=(ll)x*query;
}//树状数组更新
inline ll get_val(int x){
    ll ans=0;
    for(register int i=x;i;i-=lowbit(i))
    ans+=(ll)(x+1)*node[i]-sum[i];
    return ans;
}//树状数组查询
void change_line(int x,int y,int query)
{
    while(top[x]!=top[y])//跳到同一条链为止
    {
        if(dpt[top[x]]<dpt[top[y]])swap(x,y);
        /*优先选择链深度较深的点往上跳

        */
        add(dfn[top[x]],query);
        add(dfn[x]+1,-query);
        x=fa[top[x]];
    }
    if(dpt[x]>dpt[y])x^=y,y^=x,x^=y;
    add(dfn[x],query);add(dfn[y]+1,-query);
}//修改x,y两点路径上的点
ll get_line(int x,int y)
{
    ll ans=0;
    while(top[x]!=top[y])
    {
        if(dpt[top[x]]<dpt[top[y]])swap(x,y);
        ans+=get_val(dfn[x])-get_val(dfn[top[x]]-1);
        x=fa[top[x]];
    }
    if(dpt[x]>dpt[y])swap(x,y);
    (ans+=get_val(dfn[y])-get_val(dfn[x]-1));
    return ans;
}//同理
int main()
{
    a=read();b=read();c=read();modd=read();
    for(register int i=1;i<=a;++i)num[i]=read();
    for(register int i=1;i<a;++i)
    {
        int x,y;x=read();y=read();
        edge[++cnt]={y,head[x]},head[x]=cnt;
        edge[++cnt]={x,head[y]},head[y]=cnt;
    }
    dpt[c]=1;
    dfs1(c);dfs2(c,c);
    for(register int i=1;i<=a;++i)add(i,lct[i]-lct[i-1]);
    while(b--)
    {
        int opt,x,y,z;
        opt=read();x=read();
        switch(opt){
        case 1:
            y=read();z=read();
            change_line(x,y,z);
            break;
        case 2:
            y=read();
            printf("%lld\n",get_line(x,y)%modd);
            break;
        case 3:
            z=read();
            add(dfn[x],z);add(dfn[x]+size_[x],-z);
            break;
        case 4:
            printf("%lld\n",(get_val(dfn[x]+size_[x]-1)-get_val(dfn[x]-1))%modd);
            break;
        }
    }
    return 0;
}

六、总结

除修改两点简单路径时间复杂度为 O(log2n)

其他操作的时间复杂度为 O(logn)

证明:根据重链的定义可知,重链占子树节点的一半以上,

则每次向上查询都会减半,减少的次数正好是 log2n

总而言之,树链剖分不像是一种独立的算法,更像是一种思想,

它强调了dfn对非线性结构的线性优化作用,

能够使得树上维护更为简便、快捷。