树链剖分
那么我们首先来了解一下他可以干什么。
因为他的实现一般都要用到线段树,所以它可以进行:
两点之间最短路修改
两点之间最短路查询
以某点为根节点的子树修改
以某点为根节点的子树查询
当然这一切都基于他是一棵树。
原理嘛,很简(bu)单(shi)。
首先来了解几个定义:
重儿子:父亲节点的所有儿子中子树结点数目最多(size最大)的结点;
轻儿子:父亲节点中除了重儿子以外的儿子;
重边:父亲结点和重儿子连成的边;
轻边:父亲节点和轻儿子连成的边;
重链:由多条重边连接而成的路径;
轻链:由多条轻边连接而成的路径;
来看下面这棵树。
好像有点大了但没有关系。
按照重儿子的定义把重儿子标记出来。
有人问了你这没标点啊标的是边啊。
根据定义可以知道,重边链接的是父亲节点和他的重儿子,重链由好几条重边链接起来,所以每一条重链除链头的点以外都是重儿子(没毛病啊)。
那求出来这个有什么用呢?
问的好!
先来一大坨定义方便等下理解代码。
struct sb1{int to,next;}e[N<<1];//存放输入的边
struct sb2{int len,lz,sum;}tree[N<<2];//存放每一个节点代表的区间长度,懒标,区间内的和
int head[N];//邻接链表存图用
int deep[N];//每一个点的深度
int fa[N];//每一个点的父节点
int siz[N];//以每一个为根的子树大小
int son[N];//每一个点重儿子的标号
int dfn[N];//存放此点在线段树底下的编号
int pre[N];//下标是在线段树下面的编号,存放的是对应的在树中的序号(和dfn反着,后面代码就知道了
int w[N];//每一个点的点权
int top[N];//每一个点所在链的链头
注意 dfn 和 pre 存放的是什么,下标是什么,一开始学的时候我就一直不理解(哭。
当你把重链求出来的同时,你就会惊奇的发现轻链也求出来了,每一个叶子节点本身是一条链(不懂就算了,反正没什么用),众所周知,在树上进行一个点到另一个点经过的点的权值求和是一件非常麻烦的事情,因为一个一个找太慢了,而这个时候就需要我们的好朋友线段树了(不会请出门右转退役)。
先来一些基本的函数。
inline int ls(int x){return x<<1;}//计算左儿子的下标
inline int rs(int x){return x<<1|1;}//计算右儿子的下标
inline void p_p(int x){tree[x].sum=(tree[ls(x)].sum+tree[rs(x)].sum)%mod;}//更新x点的sum值
inline void add(int u,int v)//建边
{
e[++num].to=v;
e[num].next=head[u];
head[u]=num;
}
inline void p_d(int x)//下传懒标
{
if(tree[x].lz==0)return ;//如果懒标为0则直接退出
int zuo=ls(x),you=rs(x);//计算出左右儿子的下标
(tree[zuo].lz+=tree[x].lz)%=mod;//更新左右儿子的懒标
(tree[you].lz+=tree[x].lz)%=mod;
(tree[zuo].sum+=tree[zuo].len*tree[x].lz)%=mod;//更新左右儿子的sum值
(tree[you].sum+=tree[you].len*tree[x].lz)%=mod;
tree[x].lz=0;//清空x的懒标
}
都是线段树的一些基本操作。
我们在找重链(求 son 数组)的时候会遍历每一个点,所以可以顺手处理出 siz,deep 和 fa 数组。
void dfs1(int now,int f)//第一遍dfs,now是当前在树上的序号,f是他的父节点
{
fa[now]=f;//当前遍历到的节点的父节点
deep[now]=deep[f]+1;//计算当前点的深度
siz[now]=1;//初始只有他自己,大小为1
for(int i=head[now];i;i=e[i].next)//遍历与之相连的点
{
int v=e[i].to;//取出终点
if(v==f)continue;//如果终点是父节点就直接跳过,防止死循环
dfs1(v,now);//继续深搜
siz[now]+=siz[v];//搜完之后累加以当前点为根的子树大小
if(siz[v]>siz[son[now]]) son[now]=v;//更新重儿子
}
}
然后第二遍 dfs 处理出 dfn,pre 和 top 数组(别问我为什么不第一遍不一起处理出来,反正我不会)。
void dfs2(int now,int tp)//第二遍dfs,now是当前点在树上的序号,tp是当前点所在链的链头
{
dfn[now]=++cnt;//dfn存放在线段树底下的序号
pre[cnt]=now;//pre反着存一遍,存在线段树里面编号对应的原先树上序号
top[now]=tp;//top存当前点所在的链的链头元素编号
if(!son[now])return ;//如果没有重儿子说明没有儿子,是叶子节点,退出
dfs2(son[now],tp);//否则继续搜
for(int i=head[now];i;i=e[i].next)//遍历与当前点相连的边
{
int v=e[i].to;//取出终点
if(v==fa[now]||v==son[now])continue;//是父亲或者重儿子就跳过
dfs2(v,v);//重新开一条链搜
}
}
然后就可以建我们的线段树啦!
void build(int x,int l,int r)//x点当前在线段树的编号,lr是代表的左右区间
{
tree[x].len=r-l+1;//存点,计算区间长度
if(l==r)//如果左右边界相等
{
tree[x].sum=w[pre[l]];//直接赋值
tree[x].lz=0;//初始化懒标
return ;//退出
}
int mid=(l+r)>>1;//计算mid的值
build(ls(x),l,mid);//继续遍历左子树
build(rs(x),mid+1,r);//继续遍历右子树
p_p(x);//更新当前点的sum值
}
到了这里你就会发现,原来一棵树上的点全变成了线段树的叶子节点,而因为他每一条链都是连续的,所以查询一条链的时候就可以直接用线段树高效查询(但似乎耗费的空间更多了),所以我们本来 T 到天上的暴力与线段树融合成为了一个较为高效的做法(所以线段树真的很重要qwq)。
先来解决我学习树链剖分时的疑问:
Q:为什么建树的时候 \(w\) 的下标是 \(pre[l]\)?
A:\(w\) 数组输入的时候是按照一开始给你的树的序号输入的,而你建树时遍历到的是线段树的编号,所以需要用下标为线段树中编号,存放的是原先树上编号的 \(pre\) 数组来当下标给当前线段树中的 \(x\) 点来赋值啦。
就这一个啊
好的我们现在把我昨天咕掉的操作讲(hu shuo ba dao)一下。
首先就是线段树的区间求和,修改操作(不会请退役)。
int query(int x,int l,int r,int nl,int nr)//区间求和
{
int res=0;//总值
if(l>=nl&&r<=nr)//如果要求的区间包含此区间
return tree[x].sum;//直接返回值
p_d(x);//下传懒标
int mid=(l+r)>>1;//计算mid的值
if(nl<=mid)(res+=query(ls(x),l,mid,nl,nr))%=mod;//左边剩下的区间
if(nr> mid)(res+=query(rs(x),mid+1,r,nl,nr))%=mod;//右边剩下的区间
return res;//返回区间值
}
void update(int x,int l,int r,int nl,int nr,int v)//区间加值操作
{
if(l>=nl&&r<=nr)//要改的区间包含当前区间
{
(tree[x].sum+=tree[x].len*v)%=mod;//更新当前点sum的值
(tree[x].lz+=v)%=mod;//更新懒标
return ;//退出
}
int mid=(l+r)>>1;//计算mid的值
p_d(x);//下传懒标
if(nl<=mid)update(ls(x),l,mid,nl,nr,v);//更改左边区间
if(nr> mid)update(rs(x),mid+1,r,nl,nr,v);//更改右边区间
p_p(x);//更新当前点的sum值
}
好的我相信会线段树的人应该都会,因为代码是模板题的所以都取模了。
int fid(int x,int y)//路径求和操作
{
int ans=0;//ans存放总值
int top1=top[x],top2=top[y];//取出xy的链头元素
while(top1!=top2)//只要他俩不在同一条链上
{
if(deep[top1]<deep[top2])//保证x链头的元素深度大于y链头的元素深度
{
swap(top1,top2);//交换
swap(x,y);//顺便把xy也换了
}
(ans+=query(1,1,n,dfn[top1],dfn[x]))%=mod;//求出链头到x的距离和
x=fa[top1];top1=top[x];//更新x的点为链头的父节点,更新top1
}
if(deep[x]>deep[y])swap(x,y);//如果x点的深度大于y的深度,交换
(ans+=query(1,1,n,dfn[x],dfn[y]))%=mod;//求出x到y的距离和
return ans;//返回ans
}
void change(int x,int y,int v)//路径修改操作
{
int top1=top[x],top2=top[y];//取出xy的链头元素
while(top1!=top2)//只要他俩不在同一条链上
{
if(deep[top1]<deep[top2])//保证x链头的元素深度大于y链头的元素深度
{
swap(top1,top2);//交换
swap(x,y);//顺便把xy也换了
}
update(1,1,n,dfn[top1],dfn[x],v);//修改链头到x的点
x=fa[top1],top1=top[x];//更新x的点为链头的父节点,更新top1
}
if(deep[x]>deep[y])swap(x,y);//如果x点的深度大于y的深度,交换
update(1,1,n,dfn[x],dfn[y],v);//更新x到y的点的值
}
来模拟一下:
这个图可以顺便解决一下我之前的疑惑。
一开始我以为6的父节点是1,跳上去之后再加上4,那不就多算了1这个点吗?
然鹅并不是这样的.....
要查询4到6的简单路径长度的话,按照代码的过程来模拟,第一步就是找到4的链头4,6的链头1,可以看到当前的两点不在同一条链上,所以我们进入while循环,4比1的深度深,不需要交换,求出4到4的路径上点权的和,4跳到所在链头的父节点3,并更新top1为1,这时两点在同一条链上,退出while循环,3的深度小于6的深度,不用交换,直接计算加上3到6的距离和,最后返回ans的值,结束。
其实当你建线段树完成后,一棵树的点是都在一个连续的区间内的。
看一下模板题的完整代码:
#include<iostream>
#define int long long
#define N 100010
using namespace std;
struct sb1{int to,next;}e[N<<1];//存放输入的边
struct sb2{int len,lz,sum;}tree[N<<2];//存放每一个节点代表的区间长度,懒标,区间内的和
int head[N];//邻接链表存图用
int deep[N];//每一个点的深度
int fa[N];//每一个点的父节点
int siz[N];//以每一个为根的子树大小
int son[N];//每一个点重儿子的标号
int dfn[N];//存放此点在线段树底下的编号
int pre[N];//下标是在线段树下面的编号,存放的是对应的在树中的序号(和dfn反着,后面代码就知道了
int w[N];//每一个点的点权
int top[N];//每一个点所在链的链头
int n,m,r,mod,cnt,num;
inline int ls(int x){return x<<1;}//计算左儿子的下标
inline int rs(int x){return x<<1|1;}//计算右儿子的下标
inline void p_p(int x){tree[x].sum=(tree[ls(x)].sum+tree[rs(x)].sum)%mod;}//更新x点的sum值
inline void add(int u,int v)//建边
{
e[++num].to=v;
e[num].next=head[u];
head[u]=num;
}
void dfs1(int now,int f)//第一遍dfs,now是当前在树上的序号,f是他的父节点
{
fa[now]=f;//当前遍历到的节点的父节点
deep[now]=deep[f]+1;//计算当前点的深度
siz[now]=1;//初始只有他自己,大小为1
for(int i=head[now];i;i=e[i].next)//遍历与之相连的点
{
int v=e[i].to;//取出终点
if(v==f)continue;//如果终点是父节点就直接跳过,防止死循环
dfs1(v,now);//继续深搜
siz[now]+=siz[v];//搜完之后累加以当前点为根的子树大小
if(siz[v]>siz[son[now]]) son[now]=v;//更新重儿子
}
}
void dfs2(int now,int tp)//第二遍dfs,now是当前点在树上的序号,tp是当前点所在链的链头
{
dfn[now]=++cnt;//dfn存放在线段树底下的序号
pre[cnt]=now;//pre反着存一遍,存在线段树里面编号对应的树上序号
top[now]=tp;//top存当前点所在的链的链头元素编号
if(!son[now])return ;//如果没有重儿子说明没有儿子,是叶子节点,退出
dfs2(son[now],tp);//否则继续搜
for(int i=head[now];i;i=e[i].next)//遍历与当前点相连的边
{
int v=e[i].to;//取出终点
if(v==fa[now]||v==son[now])continue;//是父亲或者重儿子就跳过
dfs2(v,v);//重新开一条链搜
}
}
void build(int x,int l,int r)//x点当前在线段树的编号,lr是代表的左右区间
{
tree[x].len=r-l+1;//存点,计算区间长度
if(l==r)//如果左右边界相等
{
tree[x].sum=w[pre[l]];//直接赋值
tree[x].lz=0;//初始化懒标
return ;//退出
}
int mid=(l+r)>>1;//计算mid的值
build(ls(x),l,mid);//继续遍历左子树
build(rs(x),mid+1,r);//继续遍历右子树
p_p(x);//更新当前点的sum值
}
inline void p_d(int x)//下传懒标
{
if(tree[x].lz==0)return ;//如果懒标为0则直接退出
int zuo=ls(x),you=rs(x);//计算出左右儿子的下标
(tree[zuo].lz+=tree[x].lz)%=mod;//更新左右儿子的懒标
(tree[you].lz+=tree[x].lz)%=mod;
(tree[zuo].sum+=tree[zuo].len*tree[x].lz)%=mod;//更新左右儿子的sum值
(tree[you].sum+=tree[you].len*tree[x].lz)%=mod;
tree[x].lz=0;//清空x的懒标
}
int query(int x,int l,int r,int nl,int nr)//区间求和
{
int res=0;//总值
if(l>=nl&&r<=nr)//如果要求的区间包含此区间
return tree[x].sum;//直接返回值
p_d(x);//下传懒标
int mid=(l+r)>>1;//计算mid的值
if(nl<=mid)(res+=query(ls(x),l,mid,nl,nr))%=mod;//左边剩下的区间
if(nr> mid)(res+=query(rs(x),mid+1,r,nl,nr))%=mod;//右边剩下的区间
return res;//返回区间值
}
void update(int x,int l,int r,int nl,int nr,int v)//区间加值操作
{
if(l>=nl&&r<=nr)//要改的区间包含当前区间
{
(tree[x].sum+=tree[x].len*v)%=mod;//更新当前点sum的值
(tree[x].lz+=v)%=mod;//更新懒标
return ;//退出
}
int mid=(l+r)>>1;//计算mid的值
p_d(x);//下传懒标
if(nl<=mid)update(ls(x),l,mid,nl,nr,v);//更改左边区间
if(nr> mid)update(rs(x),mid+1,r,nl,nr,v);//更改右边区间
p_p(x);//更新当前点的sum值
}
int fid(int x,int y)//路径求和操作
{
int ans=0;//ans存放总值
int top1=top[x],top2=top[y];//取出xy的链头元素
while(top1!=top2)//只要他俩不在同一条链上
{
if(deep[top1]<deep[top2])//保证x链头的元素深度大于y链头的元素深度
{
swap(top1,top2);//交换
swap(x,y);//顺便把xy也换了
}
(ans+=query(1,1,n,dfn[top1],dfn[x]))%=mod;//求出链头到x的距离和
x=fa[top1];top1=top[x];//更新x的点为链头的父节点,更新top1
}
if(deep[x]>deep[y])swap(x,y);//如果x点的深度大于y的深度,交换
(ans+=query(1,1,n,dfn[x],dfn[y]))%=mod;//求出x到y的距离和
return ans;//返回ans
}
void change(int x,int y,int v)//路径修改操作
{
int top1=top[x],top2=top[y];//取出xy的链头元素
while(top1!=top2)//只要他俩不在同一条链上
{
if(deep[top1]<deep[top2])//保证x链头的元素深度大于y链头的元素深度
{
swap(top1,top2);//交换
swap(x,y);//顺便把xy也换了
}
update(1,1,n,dfn[top1],dfn[x],v);//修改链头到x的点
x=fa[top1],top1=top[x];//更新x的点为链头的父节点,更新top1
}
if(deep[x]>deep[y])swap(x,y);//如果x点的深度大于y的深度,交换
update(1,1,n,dfn[x],dfn[y],v);//更新x到y的点的值
}
signed 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);
dfs2(r,r);
build(1,1,n);
for(int i=1;i<=m;i++)
{
int x,y,z,op;
cin>>op;
if(op==1)
{
cin>>x>>y>>z;
change(x,y,z);
}
else if(op==2)
{
cin>>x>>y;
cout<<fid(x,y)<<endl;
}
else if(op==3)
{
cin>>x>>z;
update(1,1,n,dfn[x],dfn[x]+siz[x]-1,z);
}
else
{
cin>>x;
cout<<query(1,1,n,dfn[x],dfn[x]+siz[x]-1)<<endl;
}
}
return 0;
}
当进行以某一个点为根的子树的值的查询时你会发现,右边界是当前点在线段树的编号+树的大小-1,因为每一棵树的点在线段树的底部都是连续的。
好的整理完了可以去整理别的了。
边权树链剖分
上面的是点权,接下来来说一下边权的情况
总不能再建一棵树存边吧,想个办法转化一下
都存到终点上不就好了吗,除了根节点都存了边,正好 $ n-1$ 条
当第一遍dfs的时候把边存到终点
void dfs1(int now,int f)
{
fa[now]=f;
deep[now]=deep[f]+1;
siz[now]=1;
for(int i=head[now];i;i=e[i].next)
{
int v=e[i].to;
if(v==f)continue;
w[i]=e[i].w;//只要写上这个就好啦
dfs1(v,now);
siz[now]+=siz[v];
if(siz[v]>siz[son[now]]) son[now]=v;
}
}
然后在区间求和修改的地方
_maxx(1,1,n,dfn[y]+1,dfn[x])
就好啦
附赠例题P4315代码
#include<bits/stdc++.h>
#define N 100100
using namespace std;
struct sb1{int to,w,next;}e[N<<1];//存放一开始输入的边
struct sb2{int maxn,tag=-1,lz,len;}tree[N<<2];//lz是普通懒标记,tag标记是否重置为w
int n,cnt,son[N];//xon存放重儿子编号
int a[N],dfn[N],pre[N];//dfn存放在线段树的编号
int sonn[N],head[N]; //sonn
int u[N],v[N],w[N];//存放每一条路的信息
int deep[N],siz[N],fa[N],top[N];//deep存放每一个点的深度,siz存子树大小,fa存放父节点,top存放所在链的链头
string s;//输入的字符串
inline int ls(int x){return x<<1;}//计算左儿子
inline int rs(int x){return x<<1|1;}//计算右儿子
inline void p_p(int x){tree[x].maxn=max(tree[ls(x)].maxn,tree[rs(x)].maxn);}//更新当前节点的最大值
inline void add(int u,int v,int w)//加边函数
{
e[++cnt].next=head[u];
e[cnt].to=v;
e[cnt].w=w;
head[u]=cnt;
}
void dfs1(int x,int f)//dfs1
{
siz[x]=1;//存子树大小
fa[x]=f;//存放当前点的父节点
deep[x]=deep[f]+1;//计算当前节点的深度
for(int i=head[x];i;i=e[i].next)
{
int v=e[i].to;//取出当前的终点
if(v==f)continue;//如果是父节点就继续
a[v]=e[i].w;//点权转边权
dfs1(v,x);//继续搜
siz[x]+=siz[v];//回溯的时候加上siz
if(siz[v]>siz[son[x]])son[x]=v; //更新重儿子
}
}
void dfs2(int x,int tp)//dfs2,tp是x所在链的链头
{
dfn[x]=++cnt;//计算在线段树的编号
pre[cnt]=x;//反着存一遍
top[x]=tp;//存起当前x点的链头
if(!son[x])return ;//如果当前点没有重儿子就说明是叶子节点,退出
dfs2(son[x],tp);//继续搜重边
for(int i=head[x];i;i=e[i].next)
{
int v=e[i].to;//取出终点
if(v==fa[x]||v==son[x])continue;//如果是父节点或者重儿子就跳过
dfs2(v,v);//继续搜轻链
}
}
void build(int x,int l,int r)//开始建树
{
tree[x].len=r-l+1;//计算当前点存放区间的长度
if(l==r)//如果左右区间相等
{
tree[x].maxn=a[pre[l]];//赋值
return ;//退出
}
int mid=(l+r)>>1;//计算mid的值
build(ls(x),l,mid);//建左子树
build(rs(x),mid+1,r);//建右子树
p_p(x);//更新当前点的maxn
}
void p_d(int x)
{
if(tree[x].tag>=0)//如果当前要修改的话
{
tree[ls(x)].lz=tree[rs(x)].lz=0;//清空懒标,反正重置了
tree[ls(x)].maxn=tree[rs(x)].maxn=tree[x].tag;//左右儿子maxn重置
tree[ls(x)].tag=tree[rs(x)].tag=tree[x].tag;//左右儿子tag更新
tree[x].tag=-1;//清空当前点的tag
}
if(tree[x].lz)//只要懒标里面有值
{
tree[ls(x)].lz+=tree[x].lz;//左右儿子加上懒标
tree[rs(x)].lz+=tree[x].lz;
tree[ls(x)].maxn+=tree[x].lz;//因为是max所以直接加lz
tree[rs(x)].maxn+=tree[x].lz;
tree[x].lz=0;//清空懒标
}
}
int _maxx(int x,int l,int r,int nl,int nr)//求区间最大值
{
if(nl<=l&&r<=nr)return tree[x].maxn;
int mid=(l+r)>>1,res=0;
p_d(x);
if(nl<=mid)res=max(res,_maxx(ls(x),l,mid,nl,nr));
if(nr> mid)res=max(res,_maxx(rs(x),mid+1,r,nl,nr));
return res;
}
int _max(int x,int y)//求路径最大值
{
int res=0;
while(top[x]!=top[y])//只要不在一条链上
{
if(deep[top[x]]<deep[top[y]])//保证top[x]比top[y]深
swap(x,y);//交换
res=max(res,_maxx(1,1,n,dfn[top[x]],dfn[x]));//存最大值
x=fa[top[x]];//更新x
}
if(deep[x]<deep[y])swap(x,y);//同理要保证x的深度比y大
res=max(res,_maxx(1,1,n,dfn[y]+1,dfn[x]));//更新当前的最大值
return res;//返回最大值
}
void _add_(int x,int l,int r,int nl,int nr,int k)//区间修改
{
if(nl<=l&&r<=nr)
{
tree[x].lz+=k;//修改懒标
tree[x].maxn+=k;//更新maxn
return ;
}
int mid=(l+r)>>1;//计算mid的值
p_d(x);
if(nl<=mid)_add_(ls(x),l,mid,nl,nr,k);
if(nr> mid)_add_(rs(x),mid+1,r,nl,nr,k);
p_p(x);
}
void _add(int x,int y,int z)//路径修改,和上面同理
{
while(top[x]!=top[y])
{
if(deep[top[x]]<deep[top[y]])
swap(x,y);
_add_(1,1,n,dfn[top[x]],dfn[x],z);
x=fa[top[x]];
}
if(deep[x]<deep[y])
swap(x,y);
_add_(1,1,n,dfn[y]+1,dfn[x],z);
}
void cover(int x,int l,int r,int nl,int nr,int k)//区间覆盖
{
if(nl<=l&&r<=nr)
{
tree[x].maxn=k;//更新最大值
tree[x].lz=0;//懒标归零
tree[x].tag=k;//tag赋k
return ;
}
int mid=(l+r)>>1;
p_d(x);
if(nl<=mid)cover(ls(x),l,mid,nl,nr,k);
if(nr> mid)cover(rs(x),mid+1,r,nl,nr,k);
p_p(x);
}
void _cover(int x,int y,int z)//路径覆盖,和上面同理
{
while(top[x]!=top[y])
{
if(deep[top[x]]<deep[top[y]])
swap(x,y);
cover(1,1,n,dfn[top[x]],dfn[x],z);
x=fa[top[x]];
}
if(deep[x]<deep[y])
swap(x,y);
cover(1,1,n,dfn[y]+1,dfn[x],z);
}
void _change(int x,int l,int r,int p,int k)//单边修改
{
if(l==r)//到达目标点
{
tree[x].maxn=k;//覆盖
tree[x].lz=0;
tree[x].tag=k;
return ;
}
int mid=(l+r)>>1;
p_d(x);
if(p<=mid)_change(ls(x),l,mid,p,k);
else _change(rs(x),mid+1,r,p,k);
p_p(x);
}
signed main()
{
scanf("%d",&n);
for(int i=1;i<n;i++)
{
scanf("%d%d%d",&u[i],&v[i],&w[i]);
add(u[i],v[i],w[i]);//无向图存两次边
add(v[i],u[i],w[i]);
}
cnt=0;//清空cnt;
dfs1(1,0);//dfs1
dfs2(1,1);//dfs2
build(1,1,n);//建树
for(int i=1;i<n;i++)
{
if(fa[u[i]]==v[i])
sonn[i]=u[i];
else sonn[i]=v[i];//存下每一个边的边权所在点
}
while(1)
{
int x,y,z;
cin>>s;
if(s[0]=='S')break;//如果Stop就break
if(s[1]=='a')//询问最大值
{
scanf("%d%d",&x,&y);
printf("%d\n",_max(x,y));
}
if(s[1]=='d')//路径修改
{
scanf("%d%d%d",&x,&y,&z);
_add(x,y,z);
}
if(s[1]=='o')//路径覆盖
{
scanf("%d%d%d",&x,&y,&z);
_cover(x,y,z);
}
if(s[1]=='h')//单边覆盖
{
scanf("%d%d",&x,&y);
_change(1,1,n,dfn[sonn[x]],y);
}
}
return 0;
}
//by wwwaax
标签:nl,剖分,int,top,tree,mid,树链,详解,now
From: https://www.cnblogs.com/Multitree/p/16654324.html