• 『笔记』树链剖分 轻重链剖分


    前置知识

    • 最近公共祖先(LCA),树形DP,DFS序,链式前向星存图,线段树

    功能

    对一棵树进行剖分,将其分成几条链,将树形变为线性,减少处理的难度
    需要处理的问题有

    • 将树从(x)(y)结点最短路径上所有节点的值都加上(z)
    • 求树从(x)(y)结点最短路径上所有节点的值之和
    • 将以(x)为根节点的子树内所有节点值都加上(z)
    • 求以(x)为根节点的子树内所有节点值之和

    定义及概念

    • 重儿子:对于每一个非叶子节点,他的儿子中以那个儿子为根的子树的节点数最大的儿子,为该点的重儿子
    • 轻儿子:对于每个非叶子节点,他的儿子中不是重儿子的剩下的所有儿子就是轻儿子
    • 叶子节点没有重儿子也没有轻儿子,因为他根本没有儿子(QAQ)
    • 重边:一个父亲连接他的重儿子的边称为重边
    • 轻边:重边剩下的就是轻边
    • 重链:相邻的重边连起来的连接一条重儿子的链称为重链
      • 对于叶子节点,如果他是轻儿子,那么有一条以他自己为起点的长度为1的链
      • 每一条重链以其轻儿子为起点

    步骤

    (DFS1)

    功能
    • 标记每个点的深度
    • 标记每个点的父亲
    • 标记每个非叶子的子树大小(包括他自己)
    • 标记每个非叶子节点的重儿子的编号
    代码实现
    void dfs1(int x,int f,int deep)//x当前节点,f父亲,deep深度
    {
    	dep[x]=deep;//标记深度 
    	fa[x]=f;//标记每个点的父亲 
    	siz[x]=1;//标记每个非叶子节点的子树的大小 
    	int maxson=-1;//记录重儿子的儿子数量
    	for(int i=head[x];i;i=e[i].last)
    	{
    		int y=e[i].to;
    		if(y==f) continue;//如果是父亲那么就继续去找下一个
    		dfs1(y,x,deep+1);
    		siz[x]+=siz[y];//加上子树的节点数量 
    		if(siz[y]>maxson)
    		{
    			son[x]=y;
    			maxson=siz[y];//如果该子节点更大,那么就标记他的每个非叶子节点的
    			//重儿子的编号 
    		}
    	} 
    }
    

    (DFS2)

    功能
    • 标记每一个点的新编号(在线段树里面的)
    • 给每个点的新编号赋上这个点的初始值
    • 处理好每个点所在的链的顶端
    • 处理每条链
    代码实现(先处理重儿子,再处理轻儿子)
    void dfs2(int x,int topf)//x为当前的节点,topf为当前链上最顶端的节点
    {
    	id[x]=++cnt;//标记每个点的新编号 
    	wt[cnt]=w[x];//把每个点的初始值都赋到新的编号上来
    	top[x]=topf;//标记这个点所在的链的顶端
    	if(!son[x]) return;//如果没有重儿子(儿子),那么就返回
    	dfs2(son[x],topf);//先处理重儿子,在处理轻儿子,递归处理
    	for(int i=head[x];i;i=e[i].last)
    	{
    		int y=e[i].to;
    		if(y==fa[x]||y==son[x])continue;
    		//如果遍历到父亲结点或者是重儿子,那么就继续搜索
    		dfs2(y,y);
    		//每一个轻儿子都有一条从自己开始的链
    	} 
    } 
    

    处理问题

    • 先标上新的编号


    因为顺序是按照先重儿子,再轻儿子来处理的,所以每一条重链的新编号是连续的
    因为是用的(DFS)所以每一个子树的新编号也是连续的

    • 首先,当我们要处理任意两点间的路径时:
      设我们所在练的顶端深度更深的那个点为(x)
      • (ans)加上(x)点到(x)所在链的顶端这一段区间的点权和
      • (x)跳到(x)所在的链顶端的那个点的上面的那个点
      • 不断地执行这两个操作,知道这两个点处在一条链上面,然后此时在加上这两个点之间的区间和

    在这个时候我们注意到,我们所要处理的所有的区间都是连续的编号(新编号),那么我们可以用线段树处理连续编号区间和,每次查询时间复杂度为(O(log^2n))

    int qRange(int x,int y)
    {
    	int ans=0;
    	while(top[x]!=top[y])
    	{
    		if(dep[top[x]]<dep[top[y]])//把x改在所在链更深的点
    		swap(x,y);
    		res=0;
    		query(1,1,n,id[top[x]],id[x]);//ans加上x点到所在链顶端的这一区间的点权和
    		ans+=res;
    		ans%=mod;
    		x=fa[top[x]]; 
    	}
    	if(dep[x]>dep[y])swap(x,y);
    	res=0;
    	query(1,1,n,id[x],id[y]);
    	ans+=res;
    	return ans%mod; 
     } 
    

    处理一点及其子树的点权和

    记录每一个非叶子节点的子树的大小,并且每一个子树的新编号都是连续的,于是就直接线段树区间查询即可时间复杂度为(O(log n))

    int qson(int x)
    {
    	res=0;
    	query(1,1,n,id[x],id[x]+siz[x]-1);//子树的右端点为id[x]+siz[x]-1,可以手推一下
    	return res;
    }
    

    区间修改

    void updson(int x,int k)
    {
    	update(1,1,n,id[x],id[x]+siz[x]-1,k);
    }
    void updRange(int x,int y,int k)//区间修改
    {
    	k%=mod;
    	while(top[x]!=top[y])
    	{
    		if(dep[top[x]]<dep[top[y]])//让x的深度更深 
    		swap(x,y);
    		update(1,1,n,id[top[x]],id[x],k);
    		x=fa[top[x]];
    	}
    	if(dep[x]>dep[y]) swap(x,y);
    	update(1,1,n,id[x],id[y],k);
    }
    
    
    

    完整代码(200行高能)

    #include<iostream>
    #include<cstdio>
    #include<cstring>
    #include<queue>
    #include<stack>
    #include<map>
    #include<cmath>
    #include<algorithm>
    using namespace std;
    
    #define mid ((l+r)>>1)
    #define lson rt<<1,l,mid
    #define rson rt<<1|1,mid+1,r
    #define len (r-l+1)
    
    const int N=2e5+10;
    struct node{
    	int last;
    	int to;
    }e[N];
    int head[N]; 
    
    int n,m,r,mod;
    int e_cnt,w[N],wt[N];
    int a[N<<2],laz[N<<2];
    //线段树数组,懒惰标记
    int son[N],id[N],fa[N],cnt,dep[N],siz[N],top[N];
    //重儿子编号,新编号,父亲结点,dfs序,深度,子树的大小,当前链的顶端结点
    int res=0;
    void add(int from,int to)
    {
    	e[++e_cnt].last=head[from];
    	e[e_cnt].to=to;
    	head[from]=e_cnt;
    }
    //------------------------------------------------线段树
    void pushdown(int rt,int lenn)
    {
    	laz[rt<<1]+=laz[rt];
    	laz[rt<<1|1]+=laz[rt];
    	a[rt<<1]+=laz[rt]*(lenn-(lenn>>1));
    	a[rt<<1|1]+=laz[rt]*(lenn>>1);
    	a[rt<<1]%=mod;
    	a[rt<<1|1]%=mod;
    	laz[rt]=0; 
    }
    void build(int rt,int l,int r)
    {
    	if(l==r)
    	{
    		a[rt]=wt[l];//赋值点权值
    		if(a[rt]>mod) a[rt]%=mod;
    		return; 
    	}
    	build(lson);
    	build(rson);
    	a[rt]=(a[rt<<1]+a[rt<<1|1])%mod;
    }
    void query(int rt,int l,int r,int L,int R)
    {
    	if(L<=l&&r<=R)
    	{
    		res+=a[rt];
    		res%=mod;
    		return;
    	}
    	else 
    	{
    		if(laz[rt]) pushdown(rt,len);
    		if(L<=mid) query(lson,L,R);
    		if(R>mid) query(rson,L,R);
    	}
    }
    void update(int rt,int l,int r,int L,int R,int k)
    //当前节点,当前区间的左,右,要修改的区间左,右,修改值 
    {
    	if(L<=l&&r<=R)
    	{
    		laz[rt]+=k;
    		a[rt]+=k*len;
    	}
    	else
    	{
    		if(laz[rt]) pushdown(rt,len);
    		if(L<=mid) update(lson,L,R,k);
    		if(R>mid) update(rson,L,R,k);
    		a[rt]=(a[rt<<1]+a[rt<<1|1])%mod;
    	}
    } 
    //------------------------------------------------树链剖分
    int qRange(int x,int y)
    {
    	int ans=0;
    	while(top[x]!=top[y])
    	{
    		if(dep[top[x]]<dep[top[y]])//把x改在所在链更深的点
    		swap(x,y);
    		res=0;
    		query(1,1,n,id[top[x]],id[x]);//ans加上x点到所在链顶端的这一区间的点权和
    		ans+=res;
    		ans%=mod;
    		x=fa[top[x]]; 
    	}
    	if(dep[x]>dep[y])swap(x,y);
    	res=0;
    	query(1,1,n,id[x],id[y]);
    	ans+=res;
    	return ans%mod; 
     } 
    void updRange(int x,int y,int k)//区间修改
    {
    	k%=mod;
    	while(top[x]!=top[y])
    	{
    		if(dep[top[x]]<dep[top[y]])//让x的深度更深 
    		swap(x,y);
    		update(1,1,n,id[top[x]],id[x],k);
    		x=fa[top[x]];
    	}
    	if(dep[x]>dep[y]) swap(x,y);
    	update(1,1,n,id[x],id[y],k);
    }
    int qson(int x)
    {
    	res=0;
    	query(1,1,n,id[x],id[x]+siz[x]-1);
    	return res;
    }
    void updson(int x,int k)
    {
    	update(1,1,n,id[x],id[x]+siz[x]-1,k);
    }
    void dfs1(int x,int f,int deep)//x当前节点,f父亲,deep深度
    {
    	dep[x]=deep;//标记深度 
    	fa[x]=f;//标记每个点的父亲 
    	siz[x]=1;//标记每个非叶子节点的子树的大小 
    	int maxson=-1;//记录重儿子的儿子数量
    	for(int i=head[x];i;i=e[i].last)
    	{
    		int y=e[i].to;
    		if(y==f) continue;//如果是父亲那么就继续去找下一个
    		dfs1(y,x,deep+1);
    		siz[x]+=siz[y];//加上子树的节点数量 
    		if(siz[y]>maxson)
    		{
    			son[x]=y;
    			maxson=siz[y];//如果该子节点更大,那么就标记他的每个非叶子节点的
    			//重儿子的编号 
    		}
    	} 
    }
    void dfs2(int x,int topf)//x为当前的节点,topf为当前链上最顶端的节点
    {
    	id[x]=++cnt;//标记每个点的新编号 
    	wt[cnt]=w[x];//把每个点的初始值都赋到新的编号上来
    	top[x]=topf;//标记这个点所在的链的顶端
    	if(!son[x]) return;//如果没有重儿子(儿子),那么就返回
    	dfs2(son[x],topf);//先处理重儿子,在处理轻儿子,递归处理
    	for(int i=head[x];i;i=e[i].last)
    	{
    		int y=e[i].to;
    		if(y==fa[x]||y==son[x])continue;
    		//如果遍历到父亲结点或者是重儿子,那么就继续搜索
    		dfs2(y,y);
    		//每一个轻儿子都有一条从自己开始的链
    	} 
    } 
    int main()
    {
    	cin>>n>>m>>r>>mod;
    	for(int i=1;i<=n;i++)
    	cin>>w[i];//节点的初始权值 
    	for(int i=1;i<n;i++)
    	{
    		int x,y;
    		cin>>x>>y;
    		add(x,y);
    		add(y,x); 
    	}
    	dfs1(r,0,1);
    	dfs2(r,r);
    	build(1,1,n);
    	while(m--)
    	{
    		int k,x,y,z;
    		cin>>k;
    		if(k==1)
    		{
    			cin>>x>>y>>z;
    			updRange(x,y,z);
    		}
    		else if(k==2)
    		{
    			cin>>x>>y;
    			cout<<qRange(x,y)<<endl;
    		}
    		else if(k==3)
    		{
    			cin>>x>>y;
    			updson(x,y);
    		}
    		else 
    		{
    			cin>>x;
    			cout<<qson(x)<<endl;
    		}
    	}
    	return 0;
     }                         
    
  • 相关阅读:
    MySpace的六次重构
    为SQL Server 2005启用对 4 GB 以上物理内存的支持
    MyBatis拦截器自定义分页插件实现
    SpringBoot中快速实现邮箱发送
    2010全面兼容IE6IE7IE8FF的CSS HACK写法
    详细的win2003 IIS6.0 301重定向带参数的问题(实现网站的整体301跳转)
    call和apply方法
    offsetLeft、clientHeight、scrollLeft、clientLeft
    JS中的escape() & encodeURI() & encodeURIComponent() 区别
    俺的分布式架构系统之计算机网络1
  • 原文地址:https://www.cnblogs.com/1123LXY/p/14027339.html
Copyright © 2020-2023  润新知