题目大意
给出一棵 n n n个点的带点权的树,要求执行 m m m次操作:
- 改变一个点的点权;
- 询问从两点路径上所有点的权值和(包括那两个点)。
对于
60
%
60\%
60%的数据,
2
≤
n
≤
1
0
3
2 \leq n \leq 10^3
2≤n≤103,
1
≤
m
≤
1
0
3
1 \leq m \leq 10^3
1≤m≤103;
对于
100
%
100\%
100%的数据,
2
≤
n
≤
3
×
1
0
4
2 \leq n \leq 3 \times 10^4
2≤n≤3×104,
0
≤
m
≤
2
×
1
0
5
0 \leq m \leq 2 \times 10^5
0≤m≤2×105。
分析
这是一道经典的树链剖分题。我们先来讲讲树链剖分。
树链剖分
树链剖分是一种在树上的算法,它通过给树分链,可以支持更改某个或某些点的权值,查询两点的最近公共祖先(
L
C
A
LCA
LCA),查询两点路径上的点权的和或最大值或最小值……三种操作。
树链剖分的重点就在于给树分链。我们先给一棵树做一些定义:
在上图中:
- 深度:一个结点到根结点所经过边的数量,如结点 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 1→15的路径。
树链剖分的关键之处就在于树的重链。只要我们用一些数据结构(比如线段树)维护每一条链,我们就可以维护链上两点的关系,进而维护整棵树上两点的关系了。
那么我们怎么求出重边、重链,维护重链呢?其实只要两次深度优先搜索( 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时,我们给树上的节点重新编号,给重链上的点分配连续的号码。上图中的树重新编号如下:
(上图中每个结点中左边的数值为结点原来的编号,右边的数值为重新编号后的号码)
重新编号后,重链上的点就有连续的编号了。然后我们只要用一个数据结构维护新编号后的树的结点,我们就可以维护这些链,从而维护整棵树了。那么要用什么数据结构维护结点编号连续的链呢?我们自然想到了线段树。线段树支持单点、区间的修改、查询,正好适合树链剖分。这样一来,我们就可以实现树链剖分的修改、查询了。
思路
本题给出的树在进行树链剖分后,我们用线段树维护每个结点的权值。对于操作
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;
}
总结
这是一道模板题,虽然思维的要求不高,但要注意很多细节,以免失误。