【详解】树链剖分从0开始

前置知识

  • DFS序
  • 树(前向星)

简单BB:
DFS序,即DFS的顺序。
在树中,从根节点开始,往左儿子标号,一直到叶节点,在返回上个节点,往右儿子搜,又往左儿子标号……以此类推。

概念 and 操作

树链剖分,望文生义,是将一棵树分成几条链子后进行操作。
我们用树链剖分因为它的一些特性使得点与点之间的操作变得很方便。

我们分一颗树的规则是:
找出一个节点的重儿子,这堆重儿子将会连成一条一条的链,这就是重链。这棵树也将变成几条重链。

其中,重儿子的定义是一个节点的孩子中 子树节点最多的节点。与之相对的有轻儿子,在二叉树里非重儿子即轻儿子,故轻儿子就是非重儿子。
可以这么想,树链社会里,以人丁兴旺为尊,所以往往一个父亲最器重的儿子,就是子树节点最多的那个儿子;而被轻视的loser就是轻儿子。
重链就是重儿子们连成的链单独的一个节点是一条重链
在重链中,链首是做题时的一个关键,其定义就是重链的首个节点
如图所示。

这时候我们得到了几条链,再回想一下DFS序的性质,会得到在这一条链上,新的编号是连续的。
如下图:

接下来我们要对这一条链进行修改和查询操作,这让我们想起了支持区间修改和查询的线段树。只要将这棵树变成线段树,我们就可以轻松维护这一条路径了。

当我们需要对两个点进行操作(求权和,最值)时(两节点在一条链上操作时较简单,这里针对两点不在同一条链上),就可以在链之间操作。利用像LCA一样的爬树法,从一条链跳到另一条链上。
如图所示。

我想从一个蓝链上的红点到另一个绿链上的红点,只需在蓝链黄点处“跳跃”到绿链,就可到转化为同一条链上的简单操作。
那么所有树上的这样的操作问题都可如此转化了。结合代码,更好理解。

简单部分の实现

所以我们实现时需要两次预处理DFS:

  • DFS1求出每个节点的爸爸,每个节点的深度(DFS序),每个节点的子树节点大小,顺便把重儿子求了。
void DFS_1(int x,int fa,int d)
{
	size[x]=1; //子树节点个数,先算自己一个
	prt[x]=fa; //找爸爸
	dep[x]=d; //深度
	for(int i=head[x];i;i=a[i].next)
	{
		int y=a[i].to;
		if(y!=fa)
		{
			va[y]=a[i].w;  //边权转化中
			DFS_1(y,x,d+1);
			size[x]+=size[y];
			if((!son[x]) || (size[y]>size[son[x]])) //找重儿子
				son[x]=y;
		}
	}
}
  • DFS2求出链首,线段树中对应的节点编号和点权(边权变点权)
void DFS_2(int x,int t)
{
	top[x]=t; //链首
	tid[x]=++flag; //线段树中对应编号
	rank[tid[x]]=va[x]; //点权
	if(son[x]) //如果有重儿子先遍历重儿子
		DFS_2(son[x],t);
	for(int i=head[x];i;i=a[i].next)
	{
		int y=a[i].to;
		if((y!=son[x]) && (y!=prt[x])) //不是重儿子的自己是一条链
			DFS_2(y,y);
	}
}

例题(Luogu P1505[国家集训队]旅游)

Description

Ray 乐忠于旅游,这次他来到了T 城。T 城是一个水上城市,一共有 N 个景点,有些景点之间会用一座桥连接。为了方便游客到达每个景点但又为了节约成本,T 城的任意两个景点之间有且只有一条路径。换句话说, T 城中只有N − 1 座桥。Ray 发现,有些桥上可以看到美丽的景色,让人心情愉悦,但有些桥狭窄泥泞,令人烦躁。于是,他给每座桥定义一个愉悦度w,也就是说,Ray 经过这座桥会增加w 的愉悦度,这或许是正的也可能是负的。有时,Ray 看待同一座桥的心情也会发生改变。现在,Ray 想让你帮他计算从u 景点到v 景点能获得的总愉悦度。有时,他还想知道某段路上最美丽的桥所提供的最大愉悦度,或是某段路上最糟糕的一座桥提供的最低愉悦度。

Input

输入的第一行包含一个整数N,表示T 城中的景点个数。景点编号为 0…N − 1。
  接下来N − 1 行,每行三个整数u、v 和w,表示有一条u 到v,使 Ray 愉悦度增加w 的桥。桥的编号为1…N − 1。|w| <= 1000。
  输入的第N + 1 行包含一个整数M,表示Ray 的操作数目。
  接下来有M 行,每行描述了一个操作,操作有如下五种形式:
  C i w,表示Ray 对于经过第i 座桥的愉悦度变成了w。
  N u v,表示Ray 对于经过景点u 到v 的路径上的每一座桥的愉悦度都变成原来的相反数。
  SUM u v,表示询问从景点u 到v 所获得的总愉悦度。
  MAX u v,表示询问从景点u 到v 的路径上的所有桥中某一座桥所提供的最大愉悦度。
  MIN u v,表示询问从景点u 到v 的路径上的所有桥中某一座桥所提供的最小愉悦度。
  测试数据保证,任意时刻,Ray 对于经过每一座桥的愉悦度的绝对值小于等于1000。

Output

对于每一个询问(操作S、MAX 和MIN),输出答案。

Sample Input

3
0 1 1
1 2 2
8
SUM 0 2
MAX 0 2
N 0 1
SUM 0 2
MIN 0 2
C 1 3
SUM 0 2
MAX 0 2

Sample Output

3
2
1
-1
5
3

Hint

一共有10 个数据,对于第i (1 <= i <= 10) 个数据, N = M = i * 2000。


题意

给定一棵 n 个节点的树,边带权,编号 0~n-1,需要支持五种操作:

C i w 将输入的第 i 条边权值改为 w
N u v 将 u,v 节点之间的边权都变为相反数
SUM u v 询问 u,v 节点之间边权和
MAX u v 询问 u,v 节点之间边权最大值
MIN u v 询问 u,v 节点之间边权最小值
保证任意时刻所有边的权值都在[−1000,1000] 内

解析

0.注意 and BB

很烦细节和要求很多的树剖模板题
开始是HCQ巨佬叫我做这道题的,说这道题练了树剖基本也可以了,巨佬诚不欺我(

由于我是傻子所以这题改了两天。。。感谢MZH大佬对我这个debug已经de傻了的傻子的无私帮助,帮我找出了傻子错误Orz

结合上面的知识可以写出代码,有几个个人觉得值(我)得(曾)注(错)意(过)的煞笔问题点:

  • 线段树要注意l,r(查询边界)和Tree[k].l,Tree[k].r(线段树边界)的区分
  • 线段树要注意判断条件是【包含在此区间】才return,其判断条件是if((l<=Tree[k].l) && (r>=Tree[k].r))
  • 线段树注意区分Tree[k].l,Tree[k].r(边界)和Tree[2k],Tree[2k+1](左右儿子)
  • 线段树懒标记:sum负,flag去反,max,min互换后负
  • 边权转点权的过程:
    i. a[i].w(键入)
    ii. va[y](DFS1前向星中修改)
    iii. val[tid[x]]=va[x](DFS2中修改)
    iiii. ma=mi=sum=val[l](BuildTree时叶节点赋值)
  • DFS1时前向星记得判断(y!=fa)
  • 注意每次操作后的PushUp和PushDown
  • 线段树开4倍空间,前向星2倍
  • 编号从0开始!

1.操作部分代码分析 and 完整AC代码

  1. 上传更新信息PushUp
void PushUpMax(int x)
{
	int xr=Tree[2*x].ma;
	int xl=Tree[2*x+1].ma;
	Tree[x].ma=max(xr,xl);
}

void PushUpMin(int x)
{
	int xr=Tree[2*x].mi;
	int xl=Tree[2*x+1].mi;
	Tree[x].mi=min(xr,xl);	
}

void PushUpSum(int x)
{
	int xr=Tree[2*x].num;
	int xl=Tree[2*x+1].num;
	Tree[x].num=xr+xl;
}

void PushUp(int k)
{
	PushUpMax(k);
	PushUpMin(k);
	PushUpSum(k);
}
  1. C i w 简单的修改操作,和插入操作共用一个函数
void Inser(int x,int d,int k)
{
	if((d<Tree[k].l) || (d>Tree[k].r))
		return ;
	if(Tree[k].l==Tree[k].r)
	{
		Tree[k].num=Tree[k].ma=Tree[k].mi=x;		
		return ;
	}
	int mid=(Tree[k].l+Tree[k].r)>>1;
	PushDown(k);
	if(d>mid)
		Inser(x,d,2*k+1);
	else Inser(x,d,2*k);
	PushUp(k);
}

//main函数
	if(op=="C")
	{
		int i,w;
		scanf("%d%d",&i,&w);
		if(prt[Q[i].y]==Q[i].x)
			swap(Q[i].x,Q[i].y);
		Inser(w,tid[Q[i].x],1);
	}
  1. N u v 取反,懒标记的运用
void Lazy(int k)
{
	Tree[k].num*=-1;
	swap(Tree[k].ma,Tree[k].mi);
	Tree[k].ma*=-1;
	Tree[k].mi*=-1;
	Tree[k].flag=!Tree[k].flag;
}

void PushDown(int k)
{
	if(!Tree[k].flag)
		return ;

	int l=2*k;
	int r=2*k+1;

	Lazy(l);
	Lazy(r);
	
	Tree[k].flag=0;
}

void LineChange(int l,int r,int k) //线段树
{
	if((l>Tree[k].r) || (r<Tree[k].l))
		return ;
	if((l<=Tree[k].l) && (r>=Tree[k].r))
	{
		Lazy(k); //懒标记一下
		return ;	
	}
	PushDown(k);
	int mid=(Tree[k].l+Tree[k].r)>>1;
	if(Tree[k].l<=mid)
		LineChange(l,r,2*k);
	if(mid<Tree[k].r)
		LineChange(l,r,2*k+1);
	PushUp(k);
}

void Change(int x,int y)
{
	if(x==y)
		return ;
	while(top[x]!=top[y])
	{
		if(dep[top[x]]<dep[top[y]])
			swap(x,y);
		LineChange(tid[top[x]],tid[x],1);
		x=prt[top[x]];
	}
	if(dep[x]>dep[y])
		swap(x,y);
	LineChange(tid[x]+1,tid[y],1);
	return ;
}

//main函数
if(op=="N")
{
	int u;
	int v;
	scanf("%d%d",&u,&v);
	Change(u+1,v+1);
}
  1. SUM u v 求和
int LineSum(int l,int r,int k)
{
	if((l>Tree[k].r) || (r<Tree[k].l))
		return 0;
	if((l<=Tree[k].l) && (r>=Tree[k].r))
		return Tree[k].num;
	PushDown(k);
	int ans=0;
	int mid=(Tree[k].l+Tree[k].r)>>1;
	if(Tree[k].l<=mid)
		ans+=LineSum(l,r,2*k);
	if(mid<Tree[k].r)
		ans+=LineSum(l,r,2*k+1);
	return ans;
}

int AskSum(int x,int y)
{
	if(x==y)
		return -1;
	int ans=0;
	while(top[x]!=top[y])
	{
		if(dep[top[x]]<dep[top[y]])
			swap(x,y);
		ans+=LineSum(tid[top[x]],tid[x],1);
		x=prt[top[x]];
	}
	if(dep[x]>dep[y])
		swap(x,y);
	ans+=LineSum(tid[x]+1,tid[y],1);
	return ans;
}

//main函数
if(op=="SUM")
{
	int u,v;
	scanf("%d%d",&u,&v);
	int ans=AskSum(u+1,v+1);
	printf("%d\n",ans);
}

4 and 5. MAX,MIN和SUM代码基本一致,就不给了

完整AC代码太长了,点击下方的维克托小天使,看菜比的提交记录↓↓↓

你吼丫qwq

End

由于我真的菜比,所以有诸多纰漏还请指出!
谢谢您的阅读,喜欢请点个赞~

这里是我的底线o(≧口≦)o


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值