算法思想
第一次DFS
void DFS1(int u,int f)
{
Size[u]=1;d[u]=d[f]+1;//子树大小,点深度
son[u]=0;fa[u]=f;//重链节点,父节点
for(int i=head[u];i;i=e[i].next
{
int v=e[i].to;
if(v==f)continue;
DFS1(v,u);
Size[u]+=Size[v];
if(Size[son[u]]<Size[v]) son[u]=v;
}
}
第二次DFS
u和f[u]的重子top相同
void DFS2(int u,int topu)
{
top[u]=topu;
if(son[u])DFS2(son[u],topu);
for(int i=head[u];i;i=e[i].next)
if(e[i].to!=f[u]&&e[i].to!=son[u])
DFS2(e[i].to,e[i].to);//开启一条新重链
}
先DFS1,后DFS2
树链剖分与LCA
LCA的定义不再赘述,这里给出用树链剖分求出LCA的思路(如图)
- 对于给定的节点对(x,y),若x和y在同一条重链中,LCA为深度较小点
- 否则将top深度较大的点往上跳到top的父亲,这一步跳过了一条轻边
- 重复第二步直到x和y在一条重链中
代码
int LCA(int x,int y)
{
while(top[x]!=top[y])//不在同一重链时
{
if(d[top[x]]<d[top[y]])//确保x的top深度更大
swap(x,y);
x=f[top[x]];//x往上跳过一条轻边
}
return d[x]<d[y]?x:y;//返回深度小的
}
关于该算法的时间复杂度
最坏情况时,x和y的LCA为根节点,且不在一条重链上,需要将x,y到根路径上的所有轻边跳一次,但任何一个点到根路径上的重链数量和轻边数量都不超过 O ( log n ) O(\log n) O(logn),一般情况下上界不满,所以 O ( log n ) O(\log n) O(logn)的常数很小
不超过 O ( log n ) O(\log n) O(logn)的证明:
若y为x轻子,存在一个x的重子z满足Size[z]≥Size[y]
Size[z]+Size[y]<Size[x]→2Size[y]<Size[x]
每次经过一条轻边Size至少除以2,所以最多经过 O ( log n ) O(\log n) O(logn)条轻边
树链剖分与线段树
以下面的题面为例
给定一棵n个点的树,根节点为1,每个点有点权,m次操作,操作有以下四种:
1 x y z 将x~y路径上每个点点权加z
2 x z 将x子树内每个点点权加z
3 x y 查询x到y路径上每个点点权之和
4 x 查询子树内每个点点权之和
1≤n,m≤100000
关于树链剖分转线段树
树链剖分中,第二次DFS先DFS重子,再DFS轻子,将依次DFS到的点记录下来,则每条重链是一个连续区间,每个点的子树也是一个连续区间,对于子树操作,便可以转换成区间操作,可以用线段树维护,对于任意路径操作,要么在同一条重链,要么可以分解成最多log条重链+轻边,同样可以转化成区间操作,子树操作时间复杂度为
O
(
log
n
)
O(\log n)
O(logn),树链剖分完成后,每条重链相当于一段连续区间
加上线段树之后的DFS
void DFS2(int u,int topu)
{
top[u]=topu;
Nid[u]=++dfn;//原始u对应的DFS序新编号是Nid[x]
Oid[dfn]=u;//新编号Nid[u]对应的老编号为u,即Oid[Nid[u]]=u;
if(son[u])DFS2(son[u],topu);//先处理重边,确保重链区间连续
for(int i=head[u];i;i=e[i].next)
if(e[i].to!=f[u]&&e[i].to!=son[u])
DFS2(e[i].to,e[i].to);
}//如果对u为根的整个子树操作,对应的区间[Nid[u]][Nid[u]+Size[u]-1]
链处理模板
void chain(int x,int y)
{
while(top[x]!=top[y])
{
if(d[top[x]]<d[top[y]])swap(x,y);
//具体处理top[x]到x这一段,比如修改该段节点值
x=f[top[x]];
}
if(d[x]>d[y])swap(x,y);
//具体处理x~y这一段,比如修改该段节点值
}
具体例子如图(图来自LCY算法培训,侵删),每个节点保存DFS序的左右端点以及端点边权
训练
HDU2586
题目大意:在学LCA的时候已经做过,略,只讲解树链剖分的做法
思路:基本用上述已经写过的思想,注意注释地方的细节
代码
#include <iostream>
#include <cstdlib>
#include <cstdio>
#include <cmath>
#include <cstring>
using namespace std;
const int maxn=1e6;
int T,n,m,cnt,head[maxn],d[maxn],son[maxn],dis[maxn],f[maxn],Size[maxn],top[maxn];
struct node {
int to,next,w;
} e[maxn];
void Add(int to,int from,int val) {//链式前向星存边
e[++cnt].to=to;
e[cnt].w=val;
e[cnt].next=head[from];
head[from]=cnt;
}
void DFS1(int u,int fa) {//第一次DFS
Size[u]=1;
f[u]=fa;
d[u]=d[fa]+1;
son[u]=0;//以上都为初始化
for(int i=head[u]; i; i=e[i].next)
if(e[i].to!=fa) {
dis[e[i].to]=dis[u]+e[i].w;//必须先构造长度,不然下一个节点无值可用
DFS1(e[i].to,u);
Size[u]+=Size[e[i].to];
if(Size[e[i].to]>Size[son[u]])//找重链
son[u]=e[i].to;
}
}
void DFS2(int u,int topu) {
top[u]=topu;
if(son[u])DFS2(son[u],topu);
for(int i=head[u]; i; i=e[i].next)
if(e[i].to!=f[u]&&e[i].to!=son[u])
DFS2(e[i].to,e[i].to);
}
int LCA(int x,int y) {
while(top[x]!=top[y]) {
if(d[top[x]]<d[top[y]])
swap(x,y);
x=f[top[x]];//关键点,跳边
}
return d[x]<d[y]?x:y;
}
int main() {
ios::sync_with_stdio(0),cin.tie(0);
cin >>T;
while(T--) {
cin >>n>>m;
for(int i=0; i<n-1; i++) {
int u,v,k;
cin >>u>>v>>k;
Add(u,v,k);
Add(v,u,k);
}
DFS1(1,0);
DFS2(1,1);//重链初始为自己,第二个参数必须与第一个相同
while(m--) {
int u,v;
cin >>u>>v;
cout <<dis[u]+dis[v]-2*dis[LCA(u,v)]<<endl;
}
cnt=0;
memset(head,0,sizeof(head));
memset(Size,0,sizeof(Size));
memset(dis,0,sizeof(dis));
}
return 0;
}
POJ2763
题目大意:一棵树,执行两种操作,修改某边边权或者求初始点到某点的距离,并且将初始点更改为该点,输出距离
思路:由于本题为单点修改和区间求和,可以先树链剖分,之后选择树状数组/线段树求解
关于代码中的 acc+=Getsum(Nid[v])-Getsum(Nid[son[u]]-1);
这一行自我感觉有些难理解,遂画图来说明,如图,u和v已经在一条重链上,现在需要求u~v的和,代码中的son[u]为u的子节点编号,Nid[son[u]]-1又回退到了u,此时可以直接带入求出前缀和
PS: 本题卡了加速的cin和cout,浪费了我两个小时,我*你先人,cin/cout,狗都不用!
代码(树状数组)
#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
#include <cstdlib>
#include <queue>
//#include <unordered_map>
#include <map>
#include <set>
#include <numeric>
#include <stack>
#include <sstream>
#include <cmath>
#include <bitset>
//#include <unordered_set>
#include <functional>
#include <list>
#include <vector>
#include <iterator>
using namespace std;
typedef long long ll;
const int maxn=4e6+10;
int head[maxn],cnt,n,q,s,value[maxn];
//前向星头,边计数器,点数,询问数,起点,树状数组
int son[maxn],d[maxn],Nid[maxn],Oid[maxn],dfn,Size[maxn],f[maxn],top[maxn],fid[maxn],w[maxn];
//重子编号,节点深度,DFS序对应的编号,DFS序编号计数器,父节点编号,重链编号,边对应点,边权
struct node {
int to,next,id;
} e[maxn];
void Update(int pos,int t) {
for(; pos<=dfn; pos+=-pos&pos)
value[pos]+=t;
}
int Getsum(int pos) {
int sum=0;
for(; pos>0; pos-=-pos&pos)
sum+=value[pos];
return sum;
}
void DFS1(int u,int fa) {
f[u]=fa;
Size[u]=1;
d[u]=d[fa]+1;//记录层数
son[u]=0;
for(int i=head[u]; i>0; i=e[i].next) {
int v=e[i].to;
if(v!=fa) {
fid[e[i].id]=v;//边联系点
DFS1(v,u);
Size[u]+=Size[v];//统计子树大小
if(Size[v]>Size[son[u]])//找重链
son[u]=v;
}
}
}
void DFS2(int u,int topu) {
top[u]=topu;
Nid[u]=++dfn;//记录DFS序
Oid[dfn]=u;
if(son[u])DFS2(son[u],topu);//重链
for(int i=head[u]; i>0; i=e[i].next) {
int v=e[i].to;
if(v!=f[u]&&v!=son[u])
DFS2(v,v);
}
}
void Add(int to,int from,int val) {
e[++cnt].to=to;
e[cnt].id=val;
e[cnt].next=head[from];
head[from]=cnt;
}
int Sum(int u,int v) {
int acc=0;
while(top[u]!=top[v]) {
if(d[top[u]]<d[top[v]])
swap(u,v);
acc+=Getsum(Nid[u])-Getsum(Nid[top[u]]-1);//统计节点到重链点的和,这里是和线段树反过来的,由于是前缀和,所以需要-1
u=f[top[u]];
}
if(u==v)return acc;
if(d[u]>d[v])
swap(u,v);
acc+=Getsum(Nid[v])-Getsum(Nid[son[u]]-1);
return acc;
}
int main() {
// ios::sync_with_stdio(0),cin.tie();
// cin >>n>>q>>s;
scanf("%d%d%d",&n,&q,&s);
for(int i=1; i<n; i++) {
int a,b;
//cin >>a>>b>>w[i];
scanf("%d%d%d",&a,&b,&w[i]);
Add(a,b,i);
Add(b,a,i);
}
DFS1(1,0);
DFS2(1,1);//树链剖分
for(int i=1; i<n; i++)//构造树状数组
Update(Nid[fid[i]],w[i]);
while(q--) {
int choice,x,y;
//cin >>choice;
scanf("%d",&choice);
if(choice) {
//cin >>x>>y;
scanf("%d%d",&x,&y);
Update(Nid[fid[x]],y-w[x]);
w[x]=y;
} else {
// cin >>x;
// cout <<Sum(s,x)<<endl;
scanf("%d",&x);
printf("%d\n",Sum(s,x));
s=x;
}
}
return 0;
}
/*
6 100 1
1 2 1
2 6 2
1 3 3
3 4 6
4 5 7
0 6
*/
POJ3237
题目大意:给出n个节点的树,编号1~N,边编号1 ~N-1,有边权,三种操作,更改边权、置a ~b所有路径相反数、求出a ~b最大边长
思路:先树链剖分,之后直接线段树,由于是按边序号修改点权的,必须建立边编号<->点编号的索引,具体见代码
代码
#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
#include <cstdlib>
using namespace std;
typedef long long ll;
const int maxn=2e6+10;
const int INF=2000000000;//无穷大
int head[maxn],sum=1,cnt,N,d[maxn],son[maxn],Size[maxn];
//链式前向星头,边计数,dfs序计数器,节点数,深度,重链,树大小
int f[maxn],top[maxn],w[maxn],a[maxn],rk[maxn];
//父节点,重链编号,DFS序索引,点权
struct node {
int u,v,w,nt;
} e[maxn];
struct tree {
int mx,mn;
bool lazy;
} Seg[maxn<<2];
void PushUp(int rt) {
Seg[rt].mn=min(Seg[rt<<1].mn,Seg[rt<<1|1].mn);
Seg[rt].mx=max(Seg[rt<<1].mx,Seg[rt<<1|1].mx);
}
void PushDown(int rt) {
if(Seg[rt].lazy) {
Seg[rt<<1].lazy^=1;
Seg[rt<<1|1].lazy^=1;
swap(Seg[rt<<1].mx,Seg[rt<<1].mn);
Seg[rt<<1].mx*=-1;
Seg[rt<<1].mn*=-1;
swap(Seg[rt<<1|1].mx,Seg[rt<<1|1].mn);//容易错
Seg[rt<<1|1].mx*=-1;
Seg[rt<<1|1].mn*=-1;
Seg[rt].lazy=0;
}
}
void Add(int u,int v,int w) {
e[++sum].u=u;//由于边有编号,每次修改的是一条边,需要存两个端点
e[sum].v=v;
e[sum].w=w;
e[sum].nt=head[u];
head[u]=sum;
}
int Query(int L,int R,int l,int r,int rt) {
if(l>r||L>r||R<l)//越界返回极小值
return -INF;
if(L<=l&&R>=r)
return Seg[rt].mx;
int mid=(l+r)>>1;
PushDown(rt);
return max(Query(L,R,l,mid,rt<<1),Query(L,R,mid+1,r,rt<<1|1));
}
void Update1(int pos,int val,int l,int r,int rt) {
if(l==r) {
Seg[rt].mn=val;
Seg[rt].mx=val;
return;
}
PushDown(rt);
int mid=(l+r)>>1;
if(pos<=mid)Update1(pos,val,l,mid,rt<<1);
if(pos>mid)Update1(pos,val,mid+1,r,rt<<1|1);
PushUp(rt);
}
void Update2(int L,int R,int l,int r,int rt) {
if(L<=l&&r<=R) {
swap(Seg[rt].mn,Seg[rt].mx);
Seg[rt].mn*=-1;
Seg[rt].mx*=-1;
Seg[rt].lazy^=1;
return ;
}
int mid=(l+r)>>1;
PushDown(rt);
if(L<=mid)Update2(L,R,l,mid,rt<<1);
if(mid<R)Update2(L,R,mid+1,r,rt<<1|1);
PushUp(rt);
}
void DFS1(int u,int fa) {
d[u]=d[fa]+1;//记录层数
Size[u]=1;//规模
son[u]=0;//重链
f[u]=fa;//记录父节点
for(int i=head[u]; i; i=e[i].nt) {
int v=e[i].v;
if(v==fa)continue;
a[v]=e[i].w;//将该边边权存入子节点,重要
DFS1(v,u);
Size[u]+=Size[v];
if(Size[v]>Size[son[u]])son[u]=v;//获得重链
}
}
void DFS2(int u,int topu) {
top[u]=topu;
w[u]=++cnt;//记录dfs序
rk[cnt]=u;//dfs序对应原节点编号,重要
if(son[u])DFS2(son[u],topu);//重链
for(int i=head[u]; i; i=e[i].nt) {
int v=e[i].v;
if(v==f[u]||v==son[u])
continue;
DFS2(v,v);//开一条新重链
}
}
void Build(int l,int r,int rt) {
Seg[rt].lazy=0;//初始化标记
if(l==r) {
Seg[rt].mn=Seg[rt].mx=a[rk[l]];//获得dfs序对应的原节点的值
return;
}
int mid=(l+r)>>1;
Build(l,mid,rt<<1);
Build(mid+1,r,rt<<1|1);
PushUp(rt);
}
int AskMax(int u,int v) {//求得最值
int res=-INF;
while(top[u]!=top[v]) {
if(d[top[u]]<d[top[v]])
swap(u,v);
res=max(res,Query(w[top[u]],w[u],1,N,1));
u=f[top[u]];
}
if(d[u]>d[v])swap(u,v);
return max(res,Query(w[u]+1,w[v],1,N,1));
}
void Negate(int u,int v) {
while(top[u]!=top[v]) {
if(d[top[u]]<d[top[v]])
swap(u,v);
Update2(w[top[u]],w[u],1,N,1);
u=f[top[u]];
}
if(d[u]>d[v])swap(u,v);
Update2(w[u]+1,w[v],1,N,1);
}
int main() {
int t;
scanf("%d",&t);
while(t--) {
cnt=0;
sum=1;//从1开始方便后面直接取边
memset(head,0,sizeof(head));//初始化
scanf("%d",&N);
for(int i=1; i<N; i++) {
int u,v,w;
scanf("%d%d%d",&u,&v,&w);//存边
Add(u,v,w);
Add(v,u,w);
}
scanf("\n");
DFS1(1,0);
DFS2(1,1);
Build(1,N,1);
char s[10];
while(scanf("%s",s)&&s[0]!='D') {
int x,y;
scanf("%d%d\n",&x,&y);
if(s[0]=='Q')
printf("%d\n",AskMax(x,y));
else if(s[0]=='C') {
int u=e[x*2].u,v=e[x*2].v;//sum为1在这里方便直接取
if(d[u]<d[v])Update1(w[v],y,1,N,1);
else Update1(w[u],y,1,N,1);
} else
Negate(x,y);
}
}
return 0;
}
HDU3966
题目大意:一棵树,N个节点,有点权,两种操作:a ~ b路径上所有点都加上一个值;询问某点的点权
思路:树链剖分模板,唯一注意的就是点权的赋值,点编号与dfs序的对应
代码
#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
#include <cstdlib>
#include <queue>
#include <unordered_map>
#include <map>
#include <set>
#include <numeric>
#include <stack>
#include <sstream>
#include <cmath>
#include <bitset>
#include <unordered_set>
#include <functional>
#include <list>
#include <vector>
#include <iterator>
using namespace std;
typedef long long ll;
const int maxn=2e6+10;
int head[maxn],cnt,N,M,P,camp[maxn];
//链式前向星头,边计数,点数,边数,操作数,节点值
int Size[maxn],f[maxn],son[maxn],d[maxn],top[maxn];
//子树规模,父节点,重链节点,深度,重链节点
int Nid[maxn],dfn,Oid[maxn];
struct tree {
int val,lazy;
} Seg[maxn];
struct node {
int to,next;
} e[maxn];
void Add(int to,int from) {
e[++cnt].to=to;
e[cnt].next=head[from];
head[from]=cnt;
}
void DFS1(int u,int fa) {
f[u]=fa;
Size[u]=1;
son[u]=0;
d[u]=d[fa]+1;
for(int i=head[u]; i; i=e[i].next) {
int v=e[i].to;
if(v==fa)continue;
DFS1(v,u);
Size[u]+=Size[v];
if(Size[v]>Size[son[u]])son[u]=v;
}
}
void DFS2(int u,int topu) {
Nid[u]=++dfn;
top[u]=topu;
Oid[dfn]=u;
if(son[u])DFS2(son[u],topu);
for(int i=head[u]; i; i=e[i].next) {
int v=e[i].to;
if(v==f[u]||v==son[u])continue;
DFS2(v,v);
}
}
void PushDown(int l,int r,int rt) {
if(Seg[rt].lazy!=0) {
Seg[rt<<1].lazy+=Seg[rt].lazy;
Seg[rt<<1|1].lazy+=Seg[rt].lazy;
Seg[rt<<1].val+=l*Seg[rt].lazy;
Seg[rt<<1|1].val+=r*Seg[rt].lazy;
Seg[rt].lazy=0;
}
}
void Build(int l,int r,int rt) {
Seg[rt].lazy=0;//区间节点的懒标记也要更新
if(l==r) {
Seg[rt].val=camp[Oid[l]];//这个地方容易错,l是dfs的序号,需要将序号还原成节点的序号
return;
}
int mid=(l+r)>>1;
Build(l,mid,rt<<1);
Build(mid+1,r,rt<<1|1);
Seg[rt].val=Seg[rt<<1].val+Seg[rt<<1|1].val;
}
void Update(int L,int R,int v,int l,int r,int rt) {
if(l>r||l>R||L>r)
return ;
if(L<=l&&R>=r) {
Seg[rt].val+=(r-l+1)*v;
Seg[rt].lazy+=v;
return;
}
int mid=(l+r)>>1;
PushDown(mid-l+1,r-mid,rt);
Update(L,R,v,l,mid,rt<<1);
Update(L,R,v,mid+1,r,rt<<1|1);
Seg[rt].val=Seg[rt<<1].val+Seg[rt<<1|1].val;
}
int Query(int pos,int l,int r,int rt) {
if(l==r&&pos==l)
return Seg[rt].val;
int mid=(l+r)>>1;
PushDown(mid-l+1,r-mid,rt);
if(pos<=mid)return Query(pos,l,mid,rt<<1);
else return Query(pos,mid+1,r,rt<<1|1);
}
void chain(int u,int v,int w) {
while(top[u]!=top[v]) {
if(d[top[u]]<d[top[v]])
swap(u,v);
Update(Nid[top[u]],Nid[u],w,1,N,1);//重链节点到当前节点更新
u=f[top[u]];
}
if(d[u]>d[v])
swap(u,v);
Update(Nid[u],Nid[v],w,1,N,1);//在同一条重链上选择深度小到深度大
}
int main() {
while(scanf("%d%d%d",&N,&M,&P)!=EOF) {
for(int i=1; i<=N; i++)
scanf("%d",&camp[i]);
for(int i=1; i<=M; i++) {
int u,v;
scanf("%d%d",&u,&v);
Add(u,v);
Add(v,u);
}
DFS1(1,0);
DFS2(1,1);
Build(1,N,1);
while(P--) {
char s[2];
int x,y,z;
scanf("%s",s);
switch(s[0]) {
case'I':
scanf("%d%d%d",&x,&y,&z);
chain(x,y,z);
break;
case'D':
scanf("%d%d%d",&x,&y,&z);
chain(x,y,-z);//链更新
break;
case'Q':
scanf("%d",&x);
printf("%d\n",Query(Nid[x],1,N,1));//查询
break;
}
}
cnt=dfn=0;
memset(head,0,sizeof(head));
}
return 0;
}
HDU5221
题目大意:一棵树,有点权,N个节点,三种操作:选取a ~ b路径上的所有点作为已收纳;更改一个顶点状态为未收纳(如果一开始就未收纳则不变);将以x为根的所有子树都收纳(包括x)。输出每次操作后的已收纳顶点的点权和
思路:一开始尝试标记+线段树记录区间和,但是超时,参考了网上其他的代码后发现了更高效的做法,构造线段树时不构造区间和,只有进行收纳和更改操作时才利用前缀和将对应的区间更新求和,使用标记防止不必要的更新,最后根节点的和为所有已收纳节点的和,选用的数据结构为线段树,树状数组应该也可以,之后有时间会尝试写一下
代码
#include <iostream>
#include <cstring>
#include <cstdio>
#include <cstdlib>
#pragma comment(linker, "/STACK:102400000,102400000")
using namespace std;
typedef long long ll;
const int maxn=1e6+10;
int head[maxn],cnt,N,T,Q,val[maxn],pre[maxn];
//链式前向星头,计数器,点个数,例子数,操作数,前缀和
int dfn,d[maxn],Size[maxn],son[maxn],f[maxn],top[maxn],id[maxn],rk[maxn];
//dfs序计数器,深度,规模,重链节点,父,重链编号,dfs序编号,索引
struct node {
int to,next,w;
} e[maxn];
void Add(int to,int from) {//链式前向星加边
e[++cnt].to=to;
e[cnt].next=head[from];
head[from]=cnt;
}
void DFS1(int u,int fa) {
Size[u]=1;
son[u]=0;
f[u]=fa;
d[u]=d[fa]+1;
for(int i=head[u]; i; i=e[i].next) {
int v=e[i].to;
if(v==fa)continue;
DFS1(v,u);
Size[u]+=Size[v];
if(Size[son[u]]<Size[v])son[u]=v;
}
}
void DFS2(int u,int topu) {
top[u]=topu;
id[u]=++dfn;
rk[dfn]=u;
pre[dfn]=pre[dfn-1]+val[u];//获得前缀和,重要
if(son[u])DFS2(son[u],topu);
for(int i=head[u]; i; i=e[i].next) {
int v=e[i].to;
if(v==f[u]||v==son[u])continue;
DFS2(v,v);
}
}
struct tree {
int sum;
bool type;//区间更新标记
} Seg[maxn];
void PushUp(int rt) {//向上回推
Seg[rt].sum=Seg[rt<<1].sum+Seg[rt<<1|1].sum;
}
void PushDown(int l,int r,int rt) {
if(Seg[rt].type) {
Seg[rt<<1].type=Seg[rt<<1|1].type=1;
Seg[rt].type=0;
int mid=(l+r)>>1;
Seg[rt<<1].sum=pre[mid]-pre[l-1];
Seg[rt<<1|1].sum=pre[r]-pre[mid];
}
}
void Build(int l,int r,int rt) {//清空线段树的标记和区间和
Seg[rt].type=0;
Seg[rt].sum=0;
if(l==r)
return;
int mid=(l+r)>>1;
Build(l,mid,rt<<1);
Build(mid+1,r,rt<<1|1);
}
void Update1(int pos,int l,int r,int rt) {//单点更新
if(l>r||r<pos||pos<l)
return ;
if(l==r&&l==pos) {//将该点的权值置为零
Seg[rt].sum=0;
return;
}
int mid=(l+r)>>1;
PushDown(l,r,rt);
Update1(pos,l,mid,rt<<1);
Update1(pos,mid+1,r,rt<<1|1);
PushUp(rt);
}
void Update2(int L,int R,int l,int r,int rt) {//区间更新
if(l>r||r<L||R<l)
return ;
if(l>=L&&R>=r) {
Seg[rt].type=1;
Seg[rt].sum=pre[r]-pre[l-1];//通过前缀和构造区间和
return;
}
int mid=(l+r)>>1;
PushDown(l,r,rt);
Update2(L,R,l,mid,rt<<1);
Update2(L,R,mid+1,r,rt<<1|1);
PushUp(rt);
}
int Query(int l,int r,int rt) {//查询,其实该函数只运行了一次,因为先前的更新已经使得1~N记录了有效区间和
if(Seg[rt].sum)//如果有值,直接返回
return Seg[rt].sum;
else
return 0;
int mid=(l+r)>>1;
PushDown(l,r,rt);
return Query(l,mid,rt<<1)+Query(mid+1,r,rt<<1|1);
}
void chain(int u,int v) {//进行链操作
while(top[u]!=top[v]) {
if(d[top[u]]<d[top[v]])
swap(u,v);
Update2(id[top[u]],id[u],1,N,1);
u=f[top[u]];
}
if(d[u]>d[v])
swap(u,v);
Update2(id[u],id[v],1,N,1);
}
int main() {
scanf("%d",&T);
while(T--) {
scanf("%d",&N);
for(int i=1; i<=N; i++)
scanf("%d",&val[i]);
for(int i=1; i<N; i++) {
int u,v;
scanf("%d%d",&u,&v);
Add(u,v);
Add(v,u);
}
DFS1(1,0);
DFS2(1,1);
Build(1,N,1);
scanf("%d",&Q);
while(Q--) {
int a,b,c;
scanf("%d%d",&a,&b);
switch(a) {
case 1:
scanf("%d",&c);
chain(b,c);
break;
case 2:
Update1(id[b],1,N,1);
break;
case 3:
Update2(id[b],id[b]+Size[b]-1,1,N,1);
break;
}
printf("%d\n",Query(1,N,1));
}
memset(head,0,sizeof(head));
memset(pre,0,sizeof(pre));
cnt=dfn=0;
}
return 0;
}
总结
树链剖分多用于LCA以及各种关于树上路径的操作(更新,查找),常与子树或路径操作一同使用,常转化为线段树的区间操作,在写代码的时候有几个需要注意的常错点
- 多组样例输入时,需要对线段树进行清空,每个节点都需要清空标记和值
- PushDown函数中容易写错位运算,需要注意
- 链操作时需要注意范围,树状数组是求前缀和是需要-1的
- 初始化线段树时,预值应当为当前dfs编号对应的原节点编号的值