《GMOJ-Senior-2677 树A》题解

题目大意

给出一棵 n n n个点的带点权的树,要求执行 m m m次操作:

  1. 改变一个点的点权;
  2. 询问从两点路径上所有点的权值和(包括那两个点)。

对于 60 % 60\% 60%的数据, 2 ≤ n ≤ 1 0 3 2 \leq n \leq 10^3 2n103 1 ≤ m ≤ 1 0 3 1 \leq m \leq 10^3 1m103
对于 100 % 100\% 100%的数据, 2 ≤ n ≤ 3 × 1 0 4 2 \leq n \leq 3 \times 10^4 2n3×104 0 ≤ m ≤ 2 × 1 0 5 0 \leq m \leq 2 \times 10^5 0m2×105

分析

这是一道经典的树链剖分题。我们先来讲讲树链剖分。

树链剖分

树链剖分是一种在树上的算法,它通过给树分链,可以支持更改某个或某些点的权值,查询两点的最近公共祖先( L C A LCA LCA),查询两点路径上的点权的和或最大值或最小值……三种操作。
树链剖分的重点就在于给树分链。我们先给一棵树做一些定义:
图1
在上图中:

  • 深度:一个结点到根结点所经过边的数量,如结点 13 13 13的深度为 2 2 2
  • 子树大小:以一个结点为根的子树的结点的数量(包括自己),如结点 5 5 5的大小为 5 5 5
  • 重儿子:指一个非叶子结点的儿子中子树大小最大的儿子,如结点 10 10 10 5 5 5的重儿子;
  • 重边:指一个非叶子结点与它的重儿子的连边(图 1 1 1中为标红的边),如连接结点 1 1 1 5 5 5的边;
  • 重链:由重边组成的链,如结点 1 → 15 1 \rightarrow 15 115的路径。

树链剖分的关键之处就在于树的重链。只要我们用一些数据结构(比如线段树)维护每一条链,我们就可以维护链上两点的关系,进而维护整棵树上两点的关系了。

那么我们怎么求出重边、重链,维护重链呢?其实只要两次深度优先搜索( D F S DFS DFS)就可以了。我们用第一次 D F S DFS DFS求出整棵树的重边、重链,用第二次 D F S DFS DFS维护重链。

第一次 D F S DFS DFS时,我们先求出树上每一个结点的子树大小、重儿子,然后通过重儿子得到重边、重链。然后在第二次 D F S DFS DFS时,我们给树上的节点重新编号,给重链上的点分配连续的号码。上图中的树重新编号如下:
图2
(上图中每个结点中左边的数值为结点原来的编号,右边的数值为重新编号后的号码)
重新编号后,重链上的点就有连续的编号了。然后我们只要用一个数据结构维护新编号后的树的结点,我们就可以维护这些链,从而维护整棵树了。那么要用什么数据结构维护结点编号连续的链呢?我们自然想到了线段树。线段树支持单点、区间的修改、查询,正好适合树链剖分。这样一来,我们就可以实现树链剖分的修改、查询了。

思路

本题给出的树在进行树链剖分后,我们用线段树维护每个结点的权值。对于操作 1 1 1,我们只要改变线段树上相应结点的权值就可以了。但对于操作 2 2 2,我们应该怎么办呢?
这时就体现出链的作用了。我们用两个指针 a , b a,b a,b,它们一开始指向给出的结点。例如在上图给出的中求结点 14 14 14 15 15 15路径上的点的权值和。
![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly9jZG4ubHVvZ3Uub3JnL3VwbG9hZC9waWMvNjI1NDMucG5n ”图3“)
(上图中每个结点中左边的数值为结点的编号,右边的数值为结点的权值,下同)
我们发现 a , b a,b a,b并不在同一条链上,这并不好处理,于是我们要把它们转移到同一条链上。我们找到 a , b a,b a,b所在链中链顶深度较大的一条,把答案加上链上的指针所在的结点到链顶结点的路径上,所有点的权值和(包括两点),再把指针移到链顶结点的父亲。因为链上的结点重新编号后的号码连续,所以我们可以用线段树求出。例子中的过程如下:

⇓ \Downarrow

⇓ \Downarrow

现在 a , b a,b a,b在同一条链上了,我们可以用线段树轻松求出 a a a b b b的路径上所有点的权值和,然后加上以前算的答案就是结果了。

代码

根据我们的思路,可以写出如下代码:

#include<cstdio>
struct edge{int to/*终点*/,next/*下一条边*/;}e[60006];//边(记得是无向边,数组要开两倍)
struct node{int nn/*新编号*/,deep/*深度*/,size/*子树大小*/,sc/*重儿子*/,fa/*父亲*/,top/*所在链的链顶*/,w/*权值*/,last/*连出的最后一条边*/;}a[30003];//点
struct ST{int sum,l,r,mid,lc,rc;}tr[100001];//线段树
int len,pos[30003]/*新编号为x的结点位置为pos[x]*/;
void swap(int &a,int &b)//交换函数
{
	int t=a;
	a=b;
	b=t;
	return;
}
void link(int x,int y)//连边
{
	++len;
	e[len].to=y;
	e[len].next=a[x].last;
	a[x].last=len;
	return;
}
void dfs1(int x)//第一次深搜
{
	a[x].size=1;//初始化子树大小
	a[x].sc=-1;//初始化重儿子
	int maxsize=-1;
	for(int i=a[x].last;i!=-1;i=e[i].next)//枚举每一条出边
	{
		int y=e[i].to;
		if(a[y].deep==-1)//当y深度还没确定(即还没遍历到y)时
		{
			a[y].deep=a[x].deep+1;//更新y
			a[y].fa=x;
			dfs1(y);//递归
			a[x].size+=a[y].size;//更新子树大小
			if(a[y].size>maxsize)//找重儿子
			{
				maxsize=a[y].size;
				a[x].sc=y;
			}
		}
	}
	return;
}
void dfs2(int x)//第一次深搜
{
	++len;
	a[x].nn=len;//赋予新编号
	pos[len]=x;//更新pos
	if(a[x].sc!=-1)//当有重儿子(即有儿子)时
	{
		a[a[x].sc].top=a[x].top;//更新重儿子的链顶结点位置
		dfs2(a[x].sc);//递归重儿子
		for(int i=a[x].last;i!=-1;i=e[i].next)//枚举每一条出边
		{
			int y=e[i].to;
			if(a[y].deep==a[x].deep+1&&y!=a[x].sc)//当y深度为x的加一(即y是x儿子)且y不是x重儿子时
			{
				a[y].top=y;//更新y链顶结点位置(自己)
				dfs2(y);//递归非重儿子
			}
		}
	}
	return;
}
void build(int l,int r)//构建线段树
{
	int now=len;
	tr[now].l=l;
	tr[now].r=r;
	tr[now].mid=(l+r)/2;
	if(l<r)
	{
		tr[now].lc=(++len);
		build(l,tr[now].mid);
		tr[now].rc=(++len);
		build(tr[now].mid+1,r);
		tr[now].sum=tr[tr[now].lc].sum+tr[tr[now].rc].sum;
	}
	else
	{
		tr[now].sum=a[pos[tr[now].mid]].w;//注意赋的值
	}
	return;
}
void change(int now,int p,int num)//线段树的单点修改
{
	if(tr[now].l==p&&tr[now].r==p)
	{
		tr[now].sum=num;
	}
	else
	{
		if(p<=tr[now].mid)
		{
			change(tr[now].lc,p,num);
		}
		else
		{
			change(tr[now].rc,p,num);
		}
		tr[now].sum=tr[tr[now].lc].sum+tr[tr[now].rc].sum;
	}
	return;
}
int ask(int now,int l,int r)//线段树的区间查询
{
	if(tr[now].l==l&&tr[now].r==r)
	{
		return tr[now].sum;
	}
	if(r<=tr[now].mid)
	{
		return ask(tr[now].lc,l,r);
	}
	if(tr[now].mid+1<=l)
	{
		return ask(tr[now].rc,l,r);
	}
	return ask(tr[now].lc,l,tr[now].mid)+ask(tr[now].rc,tr[now].mid+1,r);
}
int main()
{
	int n,m;
	scanf("%d%d",&n,&m);///读入n,m
	for(int i=1;i<=n;i++)//初始化a
	{
		a[i].last=-1;
		a[i].deep=-1;
	}
	len=0;
	for(int i=1;i<=n-1;i++)
	{
		int x,y;
		scanf("%d%d",&x,&y);//读入边
		link(x,y);//建边(注意是无向边,要正反向都建,一共建两次)
		link(y,x);
	}
	a[1].deep=0;
	dfs1(1);//第一次深搜
	a[1].top=1;
	len=0;
	dfs2(1);//第二次深搜
	for(int i=1;i<=n;i++)//读入权值
	{
		scanf("%d",&a[i].w);
	}
	len=1;
	build(1,n);//构建线段树
	for(int i=1;i<=m;i++)
	{
		int k,A,B;
		scanf("%d%d%d",&k,&A,&B);//读入操作
		if(k==1)//操作1
		{
			change(1,a[A].nn,B);//注意要用新的编号
		}
		else//操作2
		{
			int ans=0;
			while(a[A].top!=a[B].top)//当A,B不在同一条链上时
			{
				if(a[a[A].top].deep<a[a[B].top].deep)//比较两链链顶深度(把A所在的改为较深的)
				{
					swap(A,B);
				}
				ans+=ask(1,a[a[A].top].nn,a[A].nn);//更新答案
				A=a[a[A].top].fa;//更新A
			}
			if(a[A].deep>a[B].deep)//调整A,B的位置
			{
				swap(A,B);
			}
			printf("%d\n",ans+ask(1,a[A].nn,a[B].nn));//输出答案
		}
	}
	return 0;
}

总结

这是一道模板题,虽然思维的要求不高,但要注意很多细节,以免失误。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值