树链剖分算法讲解 + 模板 洛谷P3384 JAVA

洛谷P3384

前言:

前缀知识:1.线段树 2.邻接表
其实个人认为树链剖分挺简单的,关键是几个疑惑点需要有人解答。我就配合自己学习时碰到疑惑讲一下吧。

首先我们思考几个问题:
你有一颗树,要进行如下四个操作
1:将结点a到结点b的最短路径全部加上c。
2:求结点a到结点b的最短路中所有结点和。
3:将结点a为根结点的子树内所有的结点加上c。
4:求结点a为根结点的子树内所有结点值之和。

最容易想到的就是暴力操作,先将所有的结点深度求好。1,2操作只需要将深度大的结点往父结点走,直到a,b变成同一结点就停止,沿路对值进行操作。3,4操作就只需要dfs一下该结点下面的结点就好了。

但这些操作的复杂度是O(n),这显然太高了。如果你需要操作上百万次,那就是O(百万n)的复杂度,只要n再大点复杂度就巨高。那我们想能不能有更快的方法呢?当然有! 树链剖分!

思想:

树剖的代码量还是比较大的,讲前我们先熟悉下几个数组和概念

数组解释
son[u]保存u的重节点
fu[u]保存u的父节点
head[u]保存u的重链链顶节点
size[u]保存以u为根的所有子节点数量
id[u]保存u在dfs序中的编号
rk[u]保存dfs序编号为u的节点
deep[u]保存u在树中的深度

重儿子:父结点的所有儿子节点中size[]最大的节点。
轻儿子:除重儿子以外的所有节点。
重边:父结点与重儿子相连的边。
轻边:父结点与轻儿子相连的边。
重链:由重边连接的路径。
轻链:由轻边链接的路径。

我们需要进行两遍dfs,将这些数组求出来。
第一遍dfs我们将son[],fu[],size[],deep[]数组求出:

在这里插入图片描述

完成后大致是这样的。这里说一下,当根节点u的子节点的size[]都相同的情况下,随便选一个结点当重儿子就好。然后进行第二次的dfs,求head[],id[],rk[]。

dfs序指的是从根节点开始,该结点是第几个被跑到的。dfs序的顺序是,优先跑重儿子,重儿子的子结点跑完后再随便找轻儿子跑,我这里用id[]储存dfs序。
在这里插入图片描述
上图就是完成第二遍dfs后数组的样子了,发现没有,dfs序将你的整条重链变成了连续不间断的序号,而轻链只会有一小条,换句话说,将一条重链看做一组连续的数字,是不是每组重链都被一条“轻边”连起来了。既然这样,我们是不是可以以一组重链为区间,用线段树的区间修改与区间查询来完成操作了呢?现在你可能有感觉了,但不会具体操作,没事,我们继续讲。

如上图,我求结点6,9的最短路中得结点和。
首先我比较双方的链顶head[]是否是同一个,也就是问,两个结点是否在同一条重链上。若不是,那我就要去比较谁的链顶结点深度更大。这里6的链顶是1,9的链顶是5,那么我选择链顶深度更大的9,将链顶5到结点9这块区间的dfs序6 ~ 7区间查询出来。然后将结点9转移到链顶5的父结点上,也就是2。之后就是比对结点2,6的链顶了。链顶同样是1,那么我再去区查这块区间的dfs序2 ~ 4,整个查询操作就完成了。以下是代码。

static long c2(int a,int b)
	{
		int x=head[a];//结点a的链顶
		int y=head[b];//结点b的链顶
		long ans=0;//记录区查总值
		while (x!=y)//如果链顶不一样
		{
			if (deep[x]>deep[y])//对链顶深度更大的结点进行操作
			{
				ans=(ans+find(1,n,1,id[x],id[a]))%p;//区查从链顶到该结点的整个dfs序
				a=fu[x];//因为这块区间查完了,所以更新a结点的位置为链顶的父结点
				x=head[a];//更新目前的链顶
			}
			else
			{
				ans=(ans+find(1,n,1,id[y],id[b]))%p;
				b=fu[y];
				y=head[b];
			}
		}
		if (deep[a]>deep[b])ans=(ans+find(1,n,1,id[b],id[a]))%p;//当链顶一样后,区查该重链上的剩下区间
		else ans=(ans+find(1,n,1,id[a],id[b]))%p;
		return ans;
	}

更改操作也是同样的道理,这里就不赘述了。

另外就是查询结点u下的所有节点和的操作。其实已经理解的人看到这里已经都会操作了。我们只需要将该结点的id[u]~(id[u]+size[u]-1)这块区间查询一下就OK了。

洛谷P3384

上题目与代码!

题目描述:

如题,已知一棵包含 N 个结点的树(连通且无环),每个节点上包含一个数值,需要支持以下操作:

操作 1: 格式: 1 x y z 表示将树从 x 到 y 结点最短路径上所有节点的值都加上 z。

操作 2: 格式: 2 x y 表示求树从 x 到 y 结点最短路径上所有节点的值之和。

操作 3: 格式: 3 x z 表示将以 x 为根节点的子树内所有节点值都加上 z。

操作 4: 格式: 4 x 表示求以 x 为根节点的子树内所有节点值之和

输入格式:

第一行包含 4 个正整数 N,M,R,P,分别表示树的结点个数、操作个数、根节点序号和取模数(即所有的输出结果均对此取模)。

接下来一行包含 N 个非负整数,分别依次表示各个节点上初始的数值。

接下来 N-1 行每行包含两个整数 x,y,表示点 x 和点 y 之间连有一条边(保证无环且连通)。

接下来 M 行每行包含若干个正整数,每行表示一个操作,格式如下:

操作 1: 1 x y z;

操作 2: 2 x y;

操作 3: 3 x z;

操作 4: 4 x。

输出格式:

输出包含若干行,分别依次表示每个操作 22 或操作 44 所得的结果(对 PP 取模)。

输入输出样例:

输入:
5 5 2 24
7 3 7 8 0
1 2
1 5
3 1
4 1
3 4 2
3 2 2
4 5
1 5 1 3
2 1 3

输出:
2
21

代码:

这是70%的AC代码,剩下三个没过的是dfs爆栈了,如果用C++就能正常过,我大JAVA终究是错付了。但JAVA并不是没有过法,只需要用栈模拟一下dfs就行了,再下面会给出JAVA的模拟dfs代码。

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.StreamTokenizer;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Arrays;
import java.util.Scanner;


public class Main
{
	static int top[],next[],end[],o,n;
	static long sz[],tree[],laz[],p;
	static int fu[],son[],head[],size[],id[],rk[],deep[],cnt;
	static void lin(int a,int b)//邻接表经典板子
	{
		next[++o]=top[a];
		top[a]=o;
		end[o]=b;
	}
	
	//下面是经典线段树区修板子
	static void gai(int k,int l,int r)
	{
		if (laz[k]!=0)
		{
			tree[k<<1]=(tree[k<<1]+l*laz[k]%p)%p;
			tree[k<<1|1]=(tree[k<<1|1]+r*laz[k]%p)%p;
			laz[k<<1]=(laz[k<<1]+laz[k])%p;
			laz[k<<1|1]=(laz[k<<1|1]+laz[k])%p;
			laz[k]=0;
		}
	}
	static void con(int l,int r,int k)//初始化线段树
	{
		if (l==r)
		{
			tree[k]=sz[rk[l]]%p;//要按照dfs序维护线段树的结点,这是rk[]数组的唯一用处
			return;
		}
		int m=(l+r)>>1;
		con(l,m,k<<1);
		con(m+1,r,k<<1|1);
		tree[k]=(tree[k<<1]+tree[k<<1|1])%p;
	}
	static long find(int l,int r,int k,int a,int b)//区间查询
	{
		if (a<=l && b>=r)
		{
			return tree[k];
		}
		int m=(l+r)>>1;
		gai(k,m-l+1,r-m);
		long ans=0;
		if (a<=m)ans=(ans+find(l,m,k<<1,a,b))%p;
		if (b>m)ans=(ans+find(m+1,r,k<<1|1,a,b))%p;
		return ans;
	}
	static void add(int l,int r,int k,int a,int b,long value)//区间修改
	{
		if (a<=l && b>=r)
		{
			laz[k]=(laz[k]+value)%p;
			tree[k]=(tree[k]+(r-l+1)*value%p)%p;
			return;
		}
		int m=(l+r)>>1;
		gai(k,m-l+1,r-m);
		if (a<=m)add(l,m,k<<1,a,b,value);
		if (b>m)add(m+1,r,k<<1|1,a,b,value);
		tree[k]=(tree[k<<1]+tree[k<<1|1])%p;
	}
	
	
	
	static void dfs(int x)//第一遍dfs
	{
		size[x]=1;//初始化为1,不然如果叶子结点是0的话,根节点加了叶子结点的size[]也没用
		for (int i=top[x];i!=0;i=next[i])
		{
			if (fu[x]!=end[i])//如果不是父亲结点
			{
				deep[end[i]]=deep[x]+1;//子结点深度+1
				fu[end[i]]=x;//更改父结点
				dfs(end[i]);
				size[x]+=size[end[i]];//加上子结点的size大小
				if (size[son[x]]<size[end[i]])son[x]=end[i];//更新重儿子,因为下标是从1开始的,所以size[son[x]]默认肯定是0
			}
		}
	}
	static void dfs1(int x)
	{
		id[x]=++cnt;
		rk[cnt]=x;
		if (size[x]==1)return;//如果是叶子结点,退出dfs
		head[son[x]]=head[x];//更新重儿子的链顶
		dfs1(son[x]);//dfs序的规则,优先dfs重儿子
		for (int i=top[x];i!=0;i=next[i])
		{
			if (end[i]!=fu[x] && end[i]!=son[x])//如果不是父结点也不是重儿子
			{
				head[end[i]]=end[i];//轻儿子的链顶肯定是自身
				dfs1(end[i]);
			}
		}
	}
	static void c1(int a,int b,long value)//和c2操作没区别,不写注释了
	{
		int x=head[a];
		int y=head[b];
		while (x!=y)
		{
			if (deep[x]>deep[y])
			{
				add(1,n,1,id[x],id[a],value);
				a=fu[x];
				x=head[a];
			}
			else
			{
				add(1,n,1,id[y],id[b],value);
				b=fu[y];
				y=head[b];
			}
		}
		if (deep[a]>deep[b])add(1,n,1,id[b],id[a],value);
		else add(1,n,1,id[a],id[b],value);
	}
	static long c2(int a,int b)
	{
		int x=head[a];//结点a的链顶
		int y=head[b];//结点b的链顶
		long ans=0;//记录区查总值
		while (x!=y)//如果链顶不一样
		{
			if (deep[x]>deep[y])//对链顶深度更大的结点进行操作
			{
				ans=(ans+find(1,n,1,id[x],id[a]))%p;//区查从链顶到该结点的整个dfs序
				a=fu[x];//因为这块区间查完了,所以更新a结点的位置为链顶的父结点
				x=head[a];//更新目前的链顶
			}
			else
			{
				ans=(ans+find(1,n,1,id[y],id[b]))%p;
				b=fu[y];
				y=head[b];
			}
		}
		if (deep[a]>deep[b])ans=(ans+find(1,n,1,id[b],id[a]))%p;//当链顶一样后,区查该重链上的剩下区间
		else ans=(ans+find(1,n,1,id[a],id[b]))%p;
		return ans;
	}
	static void c3(int x,int value)
	{
		add(1,n,1,id[x],id[x]+size[x]-1,value);
	}
	static long c4(int x)
	{
		return find(1,n,1,id[x],id[x]+size[x]-1);
	}
	
	public static void main(String[] args) throws IOException
	{
		n=ini();
		int m=ini();
		int r=ini();
		o=0;//用来创建邻接表时做索引
		cnt=0;//dfs序跑到了第几个
		p=ini();
		sz=new long [n+1];
		next=new int [(m<<1)+1];
		top=new int [n+1];
		end=new int [(m<<1)+1];
		for (int i=1;i<=n;i++)sz[i]=ini();//存入值
		for (int i=1;i<n;i++)
		{
			int a=ini();
			int b=ini();
			lin(a,b);
			lin(b,a);
		}
		tree=new long[n<<2];
		laz=new long[n<<2];
		son=new int [n+1];
		head=new int [n+1];
		fu=new int [n+1];
		size=new int [n+1];
		id=new int [n+1];
		rk=new int [n+1];
		deep=new int [n+1];
		fu[r]=r;
		dfs(r);
		head[r]=r;//根节点的链顶就是自己
		dfs1(r);
		con(1,n,1);
		
		while (m-->0)
		{
			int w=ini();
			if (w==1)
			{
				c1(ini(),ini(),ini());
			}
			else if (w==2)
			{
				out.println(c2(ini(),ini()));
			}
			else if (w==3)
			{
				c3(ini(),ini());
			}
			else
			{
				out.println(c4(ini()));
			}
		}
		
		out.flush();
	}
	
	static StreamTokenizer in=new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
	static PrintWriter out=new PrintWriter(new BufferedWriter(new OutputStreamWriter(System.out)));
	static int ini() throws IOException
	{
		in.nextToken();
		return (int)in.nval;
	}
	
}

对下面模拟dfs的第23行代码稍微讲一下吧。跑到叶子结点,然后就沿着父亲结点一直回溯直到根节点,沿路更新,这个应该都能想到,那么代码中的b应该直接赋值为根节点x了,但是这样时间复杂度又有点高了。于是对更新的位置进行了优化。仔细思考你会发现,下面的情况。
在这里插入图片描述

如上图,红色代表遍历过的结点,现在遍历到了结点9,你会发现,现在的栈顶会是6,而6结点的父亲是5结点,当我们遍历到6结点的时候,栈顶的结点就是4了。我们思考之后就能很清楚的发现一个逻辑,我当前结点a要递归的目标结点b,永远会是还需要进行往下递归的结点。到时候等最后一个叶子结点的操作结束,我再一并把更新向上回溯到根就好了。

static void dfs(int x)
	{
		Stack<Integer> s=new Stack<Integer>();
		s.add(x);
		while (!s.isEmpty())
		{
			int q=s.pop();
			size[q]=1;
			boolean r=true;//默认是叶子结点
			for (int i=top[q];i!=0;i=next[i])
			{
				if (fu[q]!=end[i])//不是父亲的话
				{
					r=false;//不是叶子结点
					deep[end[i]]=deep[q]+1;//自己结点深度为父结点深度+1
					fu[end[i]]=q;//更新父节点
					s.add(end[i]);
				}
			}
			if (r)//如果是叶子结点
			{
				int a=q;//当前结点
				int b=s.isEmpty() ? x : fu[s.peek()];//此dfs最巧妙的一句,理解了就差不多了
				while (a!=b)//一直让当前结点向上更新到指定位置
				{
					size[fu[a]]+=size[a];//父节点的size增加
					if (size[a]>size[son[fu[a]]])son[fu[a]]=a;//更新重链
					a=fu[a];//向上更新
				}
			}
		}
		
	}
	static void dfs1(int x)
	{
		Stack<Integer> s=new Stack<Integer>();
		s.add(x);
		while (!s.isEmpty())
		{
			int q=s.pop();
			id[q]=++cnt;
			rk[cnt]=q;
			for (int i=top[q];i!=0;i=next[i])
			{
				if (fu[q]!=end[i] && son[q]!=end[i])
				{
					head[end[i]]=end[i];
					s.add(end[i]);
				}
			}
			if (son[q]!=0)//如果有重儿子,最后添加重儿子,因为要先跑重链,需要将重儿子添加至栈顶
			{
				head[son[q]]=head[q];
				s.add(son[q]);
			}
		}
	}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值