「树链剖分」学习笔记

一、引入

“在一棵树上进行路径的修改、求极值、求和”乍一看只要线段树就能轻松解决,实际上,仅凭线段树是不能搞定它的。我们需要用到一种貌似高级的复杂算法——「树链剖分」。

树链剖分(简称树剖),顾名思义,将一棵树划分成若干条进行处理,分为「重链剖分」和「长链剖分」,通常会搭配数据结构使用(如树状数组、线段树、主席树等)。

因为重链剖分的应用多,所以树链剖分一般默认为重链剖分

二、重链剖分

2.1 概念

  • 重儿子 / 轻儿子:定义 s i z u siz_u sizu 表示以 u u u 为根的子树的大小。对于节点 u u u 而言,它的儿子中 s i z siz siz 最大的为重儿子,其余为轻儿子。(特别地,这里定义节点 u u u 的重儿子 / 轻儿子,前提是节点 u u u 为非叶子节点)

  • 重边 / 轻边:令节点 u u u 的重儿子为 v v v,则 ( u , v ) (u,v) (u,v) 称作为重边,其余称作为轻边。

  • 重链:由重边相连组成的链。

  • 链首:每条链中深度最小的点(起点)。

  • 对于叶子节点,若其为轻儿子,则有一条以自己为起点长度为 1 1 1 的链。

  • 每一条重链以轻儿子为链首(起点)。

如下图所示(转载自洛谷日报「树链剖分」),较粗的为重边,较细的为轻边。节点编号旁边有个红色点的表明该节点是其所在链的链首。边旁的蓝色数字表示该边在线段树中的位置。

例如,图中 1 − 4 − 9 − 13 − 14 1-4-9-13-14 1491314 为一条重链。

2.2 算法实现

共需要做两次 dfs,先列出需要维护的信息:

  • fa[u],节点 u u u 的父亲,通常默认根节点为 1 1 1,且其父亲为 0 0 0

  • dep[u],节点 u 的深度,通常默认根节点的深度为 1 1 1

  • siz[u],以节点 u 为根的子树的大小。

  • hs[u],节点 u 的重儿子,若为叶子节点则赋值为 -1

  • top[u],节点 u 所在的重链的链首。

  • w[u],节点 u 在剖分完后的编号(DFS序)。

  • pos[u],节点 u 在线段树上的位置。

第一次 dfs 维护 fa[]dep[]siz[]hs[],注意初始化。

vector<int> g[N];//存边
void dfs1(int u,int fath)
{fa[u]=fath;dep[u]=dep[fath]+1;siz[u]=1;hs[u]=-1;for(auto v:g[u]){if(v==fath) continue;dfs1(v,u);siz[u]+=siz[v];if(hs[u]==-1||siz[v]>siz[hs[u]]) hs[u]=v;}
}

第二次 dfs 维护 top[]w[]pos[]

void dfs2(int u,int t)
{top[u]=t;w[u]=++cnt;pos[cnt]=u;if(hs[u]!=-1) dfs2(hs[u],t);for(auto v:g[u])if(v!=fa[u]&&hs[u]!=v)dfs2(v,v);
}

顺带一提如果要套用线段树等,记得建树时加上pos[],如下:

#define ls u<<1
#define rs u<<1|1
void build(int u,int l,int r)
{if(l==r){t[u]=a[pos[l]];return;}int mid=(l+r)>>1;build(ls,l,mid);build(rs,mid+1,r);push_up(u);
}

主函数这么打就好了:

//前面读入存图略
dfs1(1,0);//这里默认 1 为根节点,根据题目不同可以改为不同的根节点(两次 dfs 所有的 1 都要改)
dfs2(1,1);
build(1,1,n);

2.3 性质

  • 性质 1 1 1:若 v v v u u u 的轻儿子,即轻边 ( u , v ) (u,v) (u,v),则一定有 s i z v ≤ s i z u 2 siz_v \leq \frac{siz_u}{2} sizv2sizu

证明:显然,用反证法,假设 s i z v > s i z u 2 siz_v > \frac{siz_u}{2} sizv>2sizu,那么 v v v 就是 u u u 的重儿子,与 v v v u u u 的轻儿子矛盾,故 s i z v ≤ s i z u 2 siz_v \leq \frac{siz_u}{2} sizv2sizu

  • 性质 2 2 2:节点 u u u 与节点 f a t o p u fa_{top_u} fatopu 一定不在同一条重链上

证明:显然,用反证法,令 v = t o p [ u ] v=top[u] v=top[u],假设节点 v v v 与节点 f a v fa_v fav 在同一条链上,由于 v v v u u u 所在重链的链首,故 ( f a v , v ) (fa_v,v) (fav,v) 是轻边,所以 v v v f a v fa_v fav 不在同一条重链上,矛盾。

  • 性质 3 3 3:从根节点到树上任意一点的路径上的轻边数不超过 log ⁡ n \log{n} logn

证明:由上一性质可知,从根节点向下走,每经过一条轻边,以到达的节点为根的子树的节点个数至少比以上一个节点为根的子树减少一半,因此从根节点到树上任意一点的路径上的轻边数不超过 log ⁡ n \log{n} logn

  • 性质 4 4 4:从根节点到树上任意一点的路径上的重链数不超过 log ⁡ n \log{n} logn

证明:由上一性质可以,从根节点到树上任意一点的路径上的轻边数不超过 log ⁡ n \log{n} logn。显然每一条重链的两端都是轻边,因此从根节点到树上任意一点的路径上的重链数不超过 log ⁡ n \log{n} logn

2.4 时间复杂度 & 应用

  • 从根到任何一条链只需要经过 O ( log ⁡ n ) O(\log{n}) O(logn) 条链,故每次操作的时间复杂度为 O ( log ⁡ n ) O(\log{n}) O(logn)

  • 重链剖分使得每一条链上的 DFS 序是有序且连续的,这个特殊的性质对于解决一下树上问题十分的有利,也提供了一种新的方式求 LCA(最近公共祖先)。

树剖求 LCA:

不断向上跳重链,当跳到同一条重链上时,深度较小的结点即为 LCA。

int LCA(int u,int v)
{while(top[u]^top[v]){if(dep[top[u]]<dep[top[v]]) swap(u,v);//深度大的先往上跳u=fa[top[u]];}if(dep[u]<dep[v]) swap(u,v);return v;
}

2.5 例题讲解

例1. P3384 【模板】重链剖分/树链剖分

一道比较典型的树剖模板,也涵盖了一些操作的打法。

  • 1 x y z,表示将树从 x x x y y y 结点最短路径上所有节点的值都加上 z z z

  • 2 x y,表示求树从 x x x y y y 结点最短路径上所有节点的值之和。

  • 3 x z,表示将以 x x x 为根节点的子树内所有节点值都加上 z z z

  • 4 x,表示求以 x x x 为根节点的子树内所有节点值之和。

显然前两个操作属于路径上操作 / 询问,后两个操作属于以某一节点为根的子树上操作 /询问。

操作 1 1 1 与求 LCA 差不多,只不过多几步 update 罢了,对最短路 ( x , y ) (x,y) (x,y) 的操作,显然可以转换成 ( x , l c a ) (x,lca) (x,lca) ( y , l c a ) (y,lca) (y,lca) 操作,这里用树剖的话直接跳重链即可:

void modify(int u,int v,ll k)
{while(top[u]^top[v]){if(dep[top[u]]<dep[top[v]]) swap(u,v);update(1,1,n,w[top[u]],w[u],k);u=fa[top[u]];}if(dep[u]<dep[v]) swap(u,v);update(1,1,n,w[v],w[u],k);
}

操作 2 2 2 改为求和即可:

ll query(int u,int v)
{ll ans=0;while(top[u]^top[v]){if(dep[top[u]]<dep[top[v]]) swap(u,v);ans=(ans+query(1,1,n,w[top[u]],w[u]))%p;u=fa[top[u]];}if(dep[u]<dep[v]) swap(u,v);ans=(ans+query(1,1,n,w[v],w[u]))%p;return ans;
}

因为树剖后树上的 DFS 序在一条链上有序且连续的,并且以某一节点为根的子树内也是一样的,故对于操作 3 3 3 4 4 4,直接修改 / 询问 w [ u ] w[u] w[u] w [ u ] + s i z [ u ] − 1 w[u]+siz[u]-1 w[u]+siz[u]1 即可。

if(opt==3)
{scanf("%lld",&k);update(1,1,n,w[x],w[x]+siz[x]-1,k);
}
if(opt==4)printf("%lld\n",query(1,1,n,w[x],w[x]+siz[x]-1));
「完整代码」
#include<bits/stdc++.h>
#define ls u<<1
#define rs u<<1|1
#define ll long long
using namespace std;
const int N=1e5+5;
int n,m,r;
int cnt,dep[N],siz[N],hs[N],fa[N],w[N],top[N],pos[N];
ll p,rt[N],a[N],t[N<<2],tag[N<<2];
vector<int> g[N];
void dfs1(int u,int fath)
{fa[u]=fath;dep[u]=dep[fath]+1;siz[u]=1;hs[u]=-1;for(auto v:g[u]){if(v==fath) continue;dfs1(v,u);siz[u]+=siz[v];if(hs[u]==-1||siz[v]>siz[hs[u]]) hs[u]=v;}
}
void dfs2(int u,int t)
{top[u]=t;w[u]=++cnt;pos[cnt]=u;if(hs[u]!=-1) dfs2(hs[u],t);for(auto v:g[u])if(v!=fa[u]&&hs[u]!=v)dfs2(v,v);
}
void push_up(int u)
{t[u]=(t[ls]+t[rs])%p;
}
void build(int u,int l,int r)
{if(l==r){t[u]=a[pos[l]];return;}int mid=(l+r)>>1;build(ls,l,mid);build(rs,mid+1,r);push_up(u);
}
void push_down(int u,int l,int r)
{if(!tag[u]) return;tag[ls]=(tag[ls]+tag[u])%p;tag[rs]=(tag[rs]+tag[u])%p;int mid=(l+r)>>1;t[ls]=(t[ls]+tag[u]*(mid-l+1))%p;t[rs]=(t[rs]+tag[u]*(r-mid))%p;tag[u]=0;
}
void update(int u,int l,int r,int x,int y,ll k)
{if(y<l||r<x) return;if(x<=l&&r<=y){tag[u]=(tag[u]+k)%p;t[u]=(t[u]+k*(r-l+1))%p;return;}push_down(u,l,r);int mid=(l+r)>>1;if(x<=mid) update(ls,l,mid,x,y,k);if(y>mid) update(rs,mid+1,r,x,y,k);push_up(u);
}
ll query(int u,int l,int r,int x,int y)
{if(y<l||r<x) return 0;if(x<=l&&r<=y) return t[u]%p;push_down(u,l,r);int mid=(l+r)>>1;ll res=0;if(x<=mid) res=(res+query(ls,l,mid,x,y))%p;if(y>mid) res=(res+query(rs,mid+1,r,x,y))%p;return res;
}
void modify(int u,int v,ll k)
{while(top[u]^top[v]){if(dep[top[u]]<dep[top[v]]) swap(u,v);update(1,1,n,w[top[u]],w[u],k);u=fa[top[u]];}if(dep[u]<dep[v]) swap(u,v);update(1,1,n,w[v],w[u],k);
}
ll query(int u,int v)
{ll ans=0;while(top[u]^top[v]){if(dep[top[u]]<dep[top[v]]) swap(u,v);ans=(ans+query(1,1,n,w[top[u]],w[u]))%p;u=fa[top[u]];}if(dep[u]<dep[v]) swap(u,v);ans=(ans+query(1,1,n,w[v],w[u]))%p;return ans;
}
int LCA(int u,int v)
{while(top[u]^top[v]){if(dep[top[u]]<dep[top[v]]) swap(u,v);u=fa[top[u]];}if(dep[u]<dep[v]) swap(u,v);return v;
}
signed main()
{scanf("%d%d%d%lld",&n,&m,&r,&p);for(int i=1;i<=n;i++) scanf("%lld",&rt[i]);for(int i=1;i<n;i++){int u,v;scanf("%d%d",&u,&v);g[u].emplace_back(v);g[v].emplace_back(u);}dfs1(r,0);dfs2(r,r);build(1,1,n);while(m--){int opt,x,y;ll k;scanf("%d%d",&opt,&x);if(opt==1){scanf("%d%lld",&y,&k);modify(x,y,k);			}if(opt==2){scanf("%d",&y);printf("%lld\n",query(x,y));			}if(opt==3){scanf("%lld",&k);update(1,1,n,w[x],w[x]+siz[x]-1,k);}if(opt==4)printf("%lld\n",query(1,1,n,w[x],w[x]+siz[x]-1));}return 0;
}

例2. P4315 月下“毛景树”

该题需要将边权下放,转换为点权,只需在 dfs1 加多一句即可。

至于为什么要把边权转向深度更深的节点,是因为对于每一条边 ( u , v ) (u,v) (u,v),节点 v v v 有且仅会出现一次,而一个节点可能会是多个儿子的父亲,将边权下放可以有效防止信息重复导致丢失。

void dfs1(int u,int fath)
{fa[u]=fath;dep[u]=dep[fath]+1;siz[u]=1;hs[u]=-1;for(auto [v,w]:g[u])//c++17 特性 auto []{if(v==fath) continue;dfs1(v,u);siz[u]+=siz[v];a[v]=w;//边权下放为点权if(hs[u]==-1||siz[v]>siz[hs[u]]) hs[u]=v;}
}

相应的,对于询问和操作,只需要访问再最后改成 w [ v ] + 1 w[v]+1 w[v]+1 w [ u ] w[u] w[u] 即可,比,避免了将 LCA计算进去。
例如:

void modify_cha(int u,int v,int k)
{while(top[u]^top[v]){if(dep[top[u]]<dep[top[v]]) swap(u,v);change(1,1,n,w[top[u]],w[u],k);u=fa[top[u]];}if(dep[u]<dep[v]) swap(u,v);change(1,1,n,w[v]+1,w[u],k);//这里需要 w[v]+1 有效防止更新了 lca
}

其他操作没有什么改变,注意需要将初始的边存一下,对于操作 Change k w 而言,需要判断一下哪个深度深或者谁是谁的父亲再去修改较深的点即可。

if(opt[1]=='h')
{int u=U[x],v=V[x];if(fa[v]==u) swap(u,v);//等价于if(dep[v]>dep[u]) swap(u,v);change(1,1,n,w[u],w[u],y);
}
「完整代码」
#include<bits/stdc++.h>
#define ls u<<1
#define rs u<<1|1
using namespace std;
const int N=1e5+5,INF=(1<<30);
int n,U[N],V[N],pos[N],a[N],t[N<<2],tag[N<<2],lazy[N<<2];
int cnt,fa[N],dep[N],siz[N],hs[N],w[N],top[N];
char opt[15];
struct Edge
{int v,w;
};
vector<Edge> g[N];
void dfs1(int u,int fath)
{fa[u]=fath;dep[u]=dep[fath]+1;siz[u]=1;hs[u]=-1;for(auto [v,w]:g[u]){if(v==fath) continue;dfs1(v,u);siz[u]+=siz[v];a[v]=w;if(hs[u]==-1||siz[v]>siz[hs[u]]) hs[u]=v;}
}
void dfs2(int u,int t)
{top[u]=t;w[u]=++cnt;pos[cnt]=u;if(hs[u]!=-1) dfs2(hs[u],t);for(auto [v,w]:g[u])if(v!=fa[u]&&hs[u]!=v)dfs2(v,v);
}
void push_up(int u)
{t[u]=max(t[ls],t[rs]);
}
void build(int u,int l,int r)
{lazy[u]=-1;if(l==r){t[u]=a[pos[l]];return;}int mid=(l+r)>>1;build(ls,l,mid);build(rs,mid+1,r);push_up(u);
}
void push_down(int u,int l,int r)
{if(lazy[u]!=-1){tag[ls]=tag[rs]=0;t[ls]=t[rs]=lazy[ls]=lazy[rs]=lazy[u];lazy[u]=-1;}if(tag[u]){tag[ls]+=tag[u];tag[rs]+=tag[u];t[ls]+=tag[u];t[rs]+=tag[u];tag[u]=0;}
}
void change(int u,int l,int r,int x,int y,int k)
{if(y<l||r<x) return;if(x<=l&&r<=y){tag[u]=0;t[u]=lazy[u]=k;return;}push_down(u,l,r);int mid=(l+r)>>1;if(x<=mid) change(ls,l,mid,x,y,k);if(y>mid) change(rs,mid+1,r,x,y,k);push_up(u);
}
void update(int u,int l,int r,int x,int y,int k)
{if(y<l||r<x) return;if(x<=l&&r<=y){tag[u]+=k;t[u]+=k;return;}push_down(u,l,r);int mid=(l+r)>>1;if(x<=mid) update(ls,l,mid,x,y,k);if(y>mid) update(rs,mid+1,r,x,y,k);push_up(u);
}
int qmax(int u,int l,int r,int x,int y)
{if(y<l||r<x) return -INF;if(x<=l&&r<=y) return t[u];push_down(u,l,r);int mid=(l+r)>>1,res=-INF;if(x<=mid) res=max(res,qmax(ls,l,mid,x,y));if(y>mid) res=max(res,qmax(rs,mid+1,r,x,y));return res;
}
void modify_cha(int u,int v,int k)
{while(top[u]^top[v]){if(dep[top[u]]<dep[top[v]]) swap(u,v);change(1,1,n,w[top[u]],w[u],k);u=fa[top[u]];}if(dep[u]<dep[v]) swap(u,v);change(1,1,n,w[v]+1,w[u],k);
}
void modify_upd(int u,int v,int k)
{while(top[u]^top[v]){if(dep[top[u]]<dep[top[v]]) swap(u,v);update(1,1,n,w[top[u]],w[u],k);u=fa[top[u]];}if(dep[u]<dep[v]) swap(u,v);update(1,1,n,w[v]+1,w[u],k);
}
int modify_qmax(int u,int v)
{int ans=-INF;while(top[u]^top[v]){if(dep[top[u]]<dep[top[v]]) swap(u,v);ans=max(ans,qmax(1,1,n,w[top[u]],w[u]));u=fa[top[u]];}if(dep[u]<dep[v]) swap(u,v);ans=max(ans,qmax(1,1,n,w[v]+1,w[u]));return ans;
}
signed main()
{scanf("%d",&n);for(int i=1;i<n;i++){int u,v,w;scanf("%d%d%d",&u,&v,&w);g[u].emplace_back((Edge){v,w});g[v].emplace_back((Edge){u,w});U[i]=u,V[i]=v;}dfs1(1,0);dfs2(1,1);build(1,1,n);while(scanf("%s",opt)&&opt[0]!='S'){int x,y,z;scanf("%d%d",&x,&y);if(opt[1]=='h'){int u=U[x],v=V[x];if(fa[v]==u) swap(u,v);change(1,1,n,w[u],w[u],y);}if(opt[1]=='o'){scanf("%d",&z);modify_cha(x,y,z);}if(opt[1]=='d'){scanf("%d",&z);modify_upd(x,y,z);}if(opt[1]=='a') printf("%d\n",modify_qmax(x,y));}return 0;
}

例3. P2146 [NOI2015] 软件包管理器

树链剖分后,用线段树维护区间1的信息,记上一次查询线段树根节点的值为 x 1 x1 x1,本次查询线段树根节点的值为 x 2 x2 x2,那么:
a n s = ∣ x 1 − x 2 ∣ ans=|x1-x2| ans=x1x2∣
单条链网上不断修改,比较的好写。

「完整代码」
#include<bits/stdc++.h>
#define ls u<<1
#define rs u<<1|1
using namespace std;
const int N=1e5+5;
int n,Q,t[N<<2],tag[N<<2];
int cnt,fa[N],dep[N],hs[N],siz[N],w[N],top[N];
vector <int> g[N];
char opt[15];
void dfs1(int u,int fath)
{fa[u]=fath;dep[u]=dep[fath]+1;siz[u]=1;hs[u]=-1;for(auto v:g[u]){if(v==fath) continue;dfs1(v,u);siz[u]+=siz[v];if(hs[u]==-1||siz[v]>siz[hs[u]]) hs[u]=v;}
}
void dfs2(int u,int t)
{top[u]=t;w[u]=++cnt;if(hs[u]!=-1) dfs2(hs[u],t);for(auto v:g[u])if(v!=hs[u]&&fa[u]!=v)dfs2(v,v);
}
void push_up(int u)
{t[u]=t[ls]+t[rs];
}
void build(int u,int l,int r)
{tag[u]=-1;if(l==r){return;}int mid=(l+r)>>1;build(ls,l,mid);build(rs,mid+1,r);push_up(u);
}
void push_down(int u,int l,int r)
{if(tag[u]==-1) return;tag[ls]=tag[rs]=tag[u];int mid=(l+r)>>1;t[ls]=(mid-l+1)*tag[u];t[rs]=(r-mid)*tag[u];tag[u]=-1;
}
void update(int u,int l,int r,int x,int y,int k)
{if(y<l||r<x) return;if(x<=l&&r<=y){tag[u]=k;t[u]=(r-l+1)*k;return;}push_down(u,l,r);int mid=(l+r)>>1;if(x<=mid) update(ls,l,mid,x,y,k);if(y>mid) update(rs,mid+1,r,x,y,k);push_up(u);
}
void modify(int u,int v,int k)
{while(top[u]^top[v]){if(dep[top[u]]<dep[top[v]]) swap(u,v);update(1,1,n,w[top[u]],w[u],k);u=fa[top[u]];}if(dep[u]<dep[v]) swap(u,v);update(1,1,n,w[v],w[u],k);
}
signed main()
{scanf("%d",&n);for(int i=2;i<=n;i++){int u;scanf("%d",&u);++u;g[u].push_back(i);g[i].push_back(u);}dfs1(1,0);dfs2(1,1);build(1,1,n);scanf("%d",&Q);while(Q--){int x,pre;scanf("%s%d",opt,&x);++x;pre=t[1];if(opt[0]=='i'){modify(1,x,1);printf("%d\n",abs(t[1]-pre));}if(opt[0]=='u'){update(1,1,n,w[x],w[x]+siz[x]-1,0);printf("%d\n",abs(t[1]-pre));}}return 0;
}

例4. P2486 [SDOI2011] 染色

有一些思维含量的题。

统计颜色段数量时不能简单地区间加法,线段树还应维护区间最左颜色和区间最右颜色。

合并时,如果 S ( l , k ) S(l,k) S(l,k) 的右端与 S ( k + 1 , r ) S(k+1,r) S(k+1,r) 的左端颜色相同,那么 S ( l , r ) = S ( l , k ) + S ( k + 1 , r ) − 1 S(l,r)=S(l,k)+S(k+1,r)-1 S(l,r)=S(l,k)+S(k+1,r)1(减去重复的那一个);

否则 S ( l , r ) = S ( l , k ) + S ( k + 1 , r ) S(l,r)=S(l,k)+S(k+1,r) S(l,r)=S(l,k)+S(k+1,r) 正常合并。

核心代码:

void push_up(int u,int mid)
{t[u]=t[ls]+t[rs]-(c[mid]==c[mid+1]);
}
void build(int u,int l,int r)
{if(l==r){t[u]=1;return;}int mid=(l+r)>>1;build(ls,l,mid);build(rs,mid+1,r);push_up(u,mid);
}
void push_down(int u,int mid)
{if(!tag[u]) return;tag[ls]=tag[rs]=c[mid]=c[mid+1]=tag[u];t[ls]=t[rs]=1;tag[u]=0;
}
void update(int u,int l,int r,int x,int y,int k)
{if(y<l||r<x) return;if(x<=l&&r<=y){tag[u]=c[l]=c[r]=k;t[u]=1;return;}int mid=(l+r)>>1;push_down(u,mid);if(x<=mid) update(ls,l,mid,x,y,k);if(y>mid) update(rs,mid+1,r,x,y,k);push_up(u,mid);
}
int query(int u,int l,int r,int x,int y)
{if(y<l||r<x) return 0;if(x<=l&&r<=y) return t[u];int mid=(l+r)>>1,res=0;push_down(u,mid);if(x<=mid) res+=query(ls,l,mid,x,y);if(y>mid) res+=query(rs,mid+1,r,x,y);if(x<=mid&&y>mid&&c[mid]==c[mid+1]) --res;return res;
}
void modify(int u,int v,int k)
{while(top[u]^top[v]){if(dep[top[u]]<dep[top[v]]) swap(u,v);update(1,1,n,w[top[u]],w[u],k);u=fa[top[u]];}if(dep[u]<dep[v]) swap(u,v);update(1,1,n,w[v],w[u],k);
}
int query(int u,int v)
{int ans=0,x=u,y=v;while(top[u]^top[v]){if(dep[top[u]]<dep[top[v]]) swap(u,v);ans+=query(1,1,n,w[top[u]],w[u]);u=fa[top[u]];}if(dep[u]<dep[v]) swap(u,v);ans+=query(1,1,n,w[v],w[u]);u=x,v=y;while(top[u]^top[v]){if(dep[top[u]]<dep[top[v]]) swap(u,v);if(c[w[top[u]]]==c[w[fa[top[u]]]]) --ans;u=fa[top[u]];}return ans;
}

(完整代码就不贴了咕咕咕)

例5. P3979 遥远的国度

本题操作 3 3 3 看似需要换根,但是对于树剖来说换根就需要重新 dfs,不如在原先预处理出的数据上进行询问。

(此处引用了 Farkas_W 的 blog 关于树链剖分"换根操作"笔记的部分内容)

考虑分类讨论:

  • 情况 1 1 1:当 x = r o o t x=root x=root 时, x x x 就是此时整棵树的根,那么就是全局修改(查询)。
  • 情况 2 2 2:当 r o o t root root x x x 子树中时,就需要特别判断了,我们可以发现此时 x x x 的真正子树是包括除了 r o o t root root 方向上的子树之外其他所有节点。
  • 情况 3 3 3 :其他情况下 x x x 的子树以 r o o t root root 为根和以 1 1 1 为根是一样的。
int query(int u)
{if(u==root) return query(1,1,n,1,n);if(w[root]>=w[u]&&w[root]<=w[u]+siz[u]-1){int v=find(u,root);return negation_query(1,1,n,w[v],w[v]+siz[v]-1);}return query(1,1,n,w[u],w[u]+siz[u]-1);
}
「完整代码」
#include<bits/stdc++.h>
#define ls u<<1
#define rs u<<1|1
using namespace std;
const int N=1e5+5,INF=2147483647;
int n,m,root,a[N],b[N],t[N<<2],tag[N<<2];
int cnt,fa[N],dep[N],siz[N],hs[N],w[N],top[N];
vector <int> g[N];
void dfs1(int u,int fath)
{fa[u]=fath;dep[u]=dep[fath]+1;siz[u]=1;hs[u]=-1;for(auto v:g[u]){if(v==fath) continue;dfs1(v,u);siz[u]+=siz[v];if(hs[u]==-1||siz[v]>siz[hs[u]]) hs[u]=v;}
}
void dfs2(int u,int t)
{top[u]=t;w[u]=++cnt;b[cnt]=a[u];if(hs[u]!=-1) dfs2(hs[u],t);for(auto v:g[u])if(v!=hs[u]&&fa[u]!=v)dfs2(v,v);
}
void push_up(int u)
{t[u]=min(t[ls],t[rs]);
}
void build(int u,int l,int r)
{if(l==r){t[u]=b[l];return;}int mid=(l+r)>>1;build(ls,l,mid);build(rs,mid+1,r);push_up(u);
}
void push_down(int u,int l,int r)
{if(!tag[u]) return;tag[ls]=tag[rs]=tag[u];t[ls]=t[rs]=tag[u];tag[u]=0;
}
void update(int u,int l,int r,int x,int y,int k)
{if(y<l||r<x) return;if(x<=l&&r<=y){tag[u]=t[u]=k;return;}push_down(u,l,r);int mid=(l+r)>>1;if(x<=mid) update(ls,l,mid,x,y,k);if(y>mid) update(rs,mid+1,r,x,y,k);push_up(u);
}
int query(int u,int l,int r,int x,int y)
{if(y<l||r<x) return INF;if(x<=l&&r<=y) return t[u];push_down(u,l,r);int mid=(l+r)>>1,res=INF;if(x<=mid) res=min(res,query(ls,l,mid,x,y));if(y>mid) res=min(res,query(rs,mid+1,r,x,y));return res;
}
int negation_query(int u,int l,int r,int x,int y)
{if(y<l||r<x) return t[u];if(x<=l&&r<=y) return INF;push_down(u,l,r);int mid=(l+r)>>1,res=INF;res=min(res,negation_query(ls,l,mid,x,y));res=min(res,negation_query(rs,mid+1,r,x,y));return res;
}
void modify(int u,int v,int k)
{while(top[u]^top[v]){if(dep[top[u]]<dep[top[v]]) swap(u,v);update(1,1,n,w[top[u]],w[u],k);u=fa[top[u]];}if(dep[u]<dep[v]) swap(u,v);update(1,1,n,w[v],w[u],k);
}
int find(int u,int v)
{while(top[u]^top[v]){if(dep[top[u]]<dep[top[v]]) swap(u,v);if(fa[top[u]]==v) return top[u];u=fa[top[u]];}if(dep[u]<dep[v]) swap(u,v);return hs[v];
}
int query(int u)
{if(u==root) return query(1,1,n,1,n);if(w[root]>=w[u]&&w[root]<=w[u]+siz[u]-1){int v=find(u,root);return negation_query(1,1,n,w[v],w[v]+siz[v]-1);}return query(1,1,n,w[u],w[u]+siz[u]-1);
}
signed main()
{scanf("%d%d",&n,&m);for(int i=1;i<n;i++){int u,v;scanf("%d%d",&u,&v);g[u].push_back(v);g[v].push_back(u);}for(int i=1;i<=n;i++)scanf("%d",a+i);scanf("%d",&root);dfs1(root,0);dfs2(root,root);build(1,1,n);while(m--){int opt,x,y,k;scanf("%d",&opt);if(opt==1) scanf("%d",&root);if(opt==2) scanf("%d%d%d",&x,&y,&k),modify(x,y,k);if(opt==3) scanf("%d",&x),printf("%d\n",query(x));}return 0;
}

三、长链剖分

(Tip:由于长链剖分比较少用,所以这部分的篇幅会稍微短一些,绝对不是因为笔者没学长链剖分,快速带过)

3.1 概念

与重链剖分类似,我们设一个节点中深度最深的子节点为长节点,设该节点到长节点的边为重边,其他边为轻边

然后我们把首尾相连的重边组成长链,落单的一个节点也被视作一条长链。

我们就把树分成了若干条互不相交的长链。

3.2 性质

  • 性质 1 1 1:节点 u u u 与节点 f a t o p u fa_{top_u} fatopu 一定不在同一条长链上

证明:略。与重链剖分性质 2 2 2 的证明一样。

  • 性质 2 2 2:从根节点到树上任意一点的路径上的轻边数不超过 n \sqrt n n

证明:如果一个点 u u u,从一条长链跳到另一条长链上,那么跳跃到这条长链的长度不会小于之前的长链的长度。那么在最坏的情况下,长链长度分别为: 1 , 2 , 3 , . . . , n 1,2,3,...,\sqrt n 1,2,3,...,n ,也就是最多跳跃 n \sqrt n n 次,比重链剖分的 log ⁡ n \log n logn 稍劣一些。

3.3 时间复杂度 & 应用

  • 从根到任意一条链最劣需要经过 O ( n ) O(\sqrt n) O(n ) 条链,复杂度不会证(逃)。

长链剖分求树上 k k k 级祖先:

n log ⁡ n n \log n nlogn 倍增预处理求出每个节点 u u u 2 k 2^k 2k 级祖先,以及对于每条长链,从长链顶端向上 / 向下
i i i 步分别能走到哪个节点,其中 i i i 不大于长链深度。此外,预处理每个数在二进制下的最高位,记为 h i h_i hi

查询 ( u , k ) (u,k) (u,k) u u u k k k 级祖先)
首先跳到 u u u 2 h k 2^{h_k} 2hk 级祖先 v v v,由于我们预处理了从 v v v 所在长链顶端 t o p top top 向上 / 下走不超过链长步分别到哪个节点,故不难直接查询。综上,时间复杂度为 O ( n log ⁡ n ) O(n \log n) O(nlogn)

四、习题

以下为「重链剖分」习题(没有将例题讲解算在这里):

  1. P4315 月下“毛景树”
  2. P4092 [HEOI2016/TJOI2016] 树
  3. P4216 [SCOI2015] 情报传递
  4. P4069 [SDOI2016] 游戏 【前置:李超线段树】
  5. CF916E Jamie and Tree

以下为「长链剖分」习题:

  1. P5903 【模板】树上 K 级祖先
  2. CF1009F Dominant Indices

五、结语

咕咕咕,终于写完了,咕了都快一个月了(),Markdown 都快卡死了。

以后学了长链剖分再来补吧咕咕咕。

1 k 1k 1k 行不卡才怪)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/2644.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

Golang--数组、切片、映射

1、数组 1.1 数组类型 var 数组名 [数组大小]数据类型 package main import "fmt"func main(){//1、定义一个数组var arr1 [5]intarr1[0] 100arr1[1] 200fmt.Println(arr1) //[100 200 0 0 0] } 1.2 数组的初始化方式 package main import "fmt" func …

结构体对齐,位段

大家好&#xff0c;今天来给大家分享一些结构体的知识&#xff0c;结构体是我们学习数据结构的基础&#xff0c;只有把它了解清楚才能让我们学习数据结构是得心应手&#xff0c;现在让我们来看看它的一些内容吧。 1.结构体的定义和调用我们就跳过吧 大家如果还不熟悉的话可以去…

ElementUI中el-table双击单元格显示输入框

效果图 实现 <el-table:data"formData.products"row-key"id":show-header"true"style"width: 100%; margin-top: 16px"class"zq-table-theme-info"bordercell-dblclick"handleDbClick"> <el-table-col…

Python OpenCV 图像改变

更改图像数据 通过 改像素点 或者 切片的区域 import cv2 import numpy as np img cv2.imread("image.jpg") print(img[3,5]) # 显示某位置(行3列5)的像素值( 如 [53 34 29] 它是有三通道 B G R 组成) img[3,5] (0,0,255) # 更改该位置的像素…

学习虚幻C++开发日志——定时器

官方文档&#xff1a;虚幻引擎中的Gameplay定时器 | 虚幻引擎 5.5 文档 | Epic Developer Community | Epic Developer Community 定时器 安排在经过一定延迟或一段时间结束后要执行的操作。例如&#xff0c;您可能希望玩家在获取某个能力提升道具后变得无懈可击&#xff0c;…

网络安全设备Bypass功能介绍及分析

网络安全平台厂商往往需要用到一项比较特殊的技术&#xff0c;那就是Bypass&#xff0c;那么到底什么是Bypass呢&#xff0c;Bypass设备又是如何来实现的&#xff1f;下面我就对Bypass技术做一下简单的介绍和说明。 一、 什么是Bypass。 大家知道&#xff0c;网络安全设备一般…

如何更改Android studio的项目存储路径

如果你希望永久更改Android Studio的默认项目保存路径&#xff0c;可以通过以下步骤进行设置&#xff1a; 打开Android Studio&#xff0c;选择“File”菜单下的“Settings”&#xff08;Windows&#xff09;或“Preferences”&#xff08;Mac&#xff09;。在设置窗口中&…

ESP8266 自定义固件烧录-mqtt透传固件

esp8266 mqtt固件配网及使用说明_哔哩哔哩_bilibilihttps://www.bilibili.com/video/BV196421G7Xc/?spm_id_from333.999.0.0一、固件介绍 固件为自定义开发的一个适配物联网项目的开源固件&#xff0c;支持网页配网、支持网页mqtt服务器配置、支持主题设置。 方便、快捷、稳…

二十三、Mysql8.0高可用集群架构实战

文章目录 一、MySQL InnoDB Cluster1、基本概述2、集群架构3、搭建一主两从InnoDB集群3.1、 安装3个数据库实例3.2、安装mysqlrouter和安装mysqlshell3.2.1、安装mysql-router3.2.2、安装mysql-shell 3.3、InnoDB Cluster 初始化3.1 参数及权限配置预需求检测3.2 初始化InnoDB …

[OS] mmap() 函数的参数及其作用

参数说明&#xff1a; addr&#xff1a;映射区域的起始地址。如果设置为 0&#xff0c;则由内核自动选择页对齐的地址。length&#xff1a;需要映射的字节数&#xff0c;决定映射的区域大小。prot&#xff1a;映射区域的内存保护属性&#xff0c;如只读、可读写等。这个属性不…

meta-learning based FD论文阅读笔记

[1]Semi-Supervised Temporal Meta-Learning Framework for Wind Turbine Bearing Fault Diagnosis Under Limited Annotation Data 问题背景 the fault data are so scarce that it is time-consuming to acquire a well behaved deep learning modelmuch unlabeled data ca…

web渗透——小白入狱

目录 理论知识总结一、Web渗透核心知识点二、Web渗透实操案例三、Web渗透学习建议实操案例一、信息收集实操步骤&#xff1a; 二、SQL注入实操步骤&#xff1a; 三、跨站脚本攻击&#xff08;XSS&#xff09;实操步骤&#xff1a; 四、CSRF攻击实操步骤&#xff1a; 五、本地文…

一个完整的产品级物联网系统在农业领域的应用,通过传感器、通信、云计算和控制设备的协同工作,实现了智能化的农业灌溉管理

以下为您详细介绍一个智能农业灌溉系统作为产品级的物联网实际案例&#xff1a; **一、项目背景** 随着农业现代化的发展&#xff0c;精准灌溉对于提高农作物产量、节约水资源具有重要意义。传统的灌溉方式往往依赖人工经验&#xff0c;效率低下且浪费水资源。因此&#xff0c…

JeecgBoot入门

最近在了解低代码平台&#xff0c;其中关注到gitee上开源项目JeecgBoot&#xff0c;JeecgBoot官方也有比较完整的入门教学文档&#xff0c;这里我们将耕者官方教程学习&#xff0c;并将其记录下来。 一、项目简介 JeecgBoot 是一款基于代码生成器的低代码开发平台拥有零代码能力…

qt QEvent详解

1、概述 QEvent是Qt框架中事件机制的基础类。在Qt中&#xff0c;事件是由底层窗口系统&#xff08;如Windows、Linux的X11、macOS的Cocoa等&#xff09;生成的&#xff0c;Qt的主事件循环&#xff08;QCoreApplication::exec()&#xff09;负责从事件队列中获取这些事件&#…

#Jest进阶知识:整合 webpack 综合练习

这一小节&#xff0c;我们来做一个综合的练习&#xff0c;该练习会整合&#xff1a; typescriptwebpackjest 准备工作 首先创建项目目录&#xff0c;通过 npm init -y 进行初始化。 整个项目我们打算使用 typescript 进行开发&#xff0c;因此需要安装 typescript npm i t…

【安卓13 源码】Input子系统(4)- InputReader 数据处理

1. 多指触控协议 多指触控协议有 2 种&#xff1a; > A类&#xff1a; 处理无关联的接触&#xff1a; 用于直接发送原始数据&#xff1b; > B类&#xff1a; 处理跟踪识别类的接触&#xff1a; 通过事件slot发送相关联的独立接触更新。 B协议可以使用一个ID来标识触点&…

VMware 虚拟机使用教程及 Kali Linux 安装指南

VMware 虚拟机使用教程及 Kali Linux 安装指南 在现代计算机科学与网络安全领域&#xff0c;虚拟化技术的应用越来越广泛。VMware 是一款功能强大的虚拟化软件&#xff0c;可以帮助用户在同一台物理机上运行多个操作系统。本文将详细介绍如何使用 VMware 虚拟机&#xff0c;并…

工业通信网关的各项功能解析-天拓四方

在工业自动化和智能制造的浪潮中&#xff0c;工业通信网关作为连接工业现场与互联网的重要桥梁&#xff0c;发挥着至关重要的作用。它不仅实现了不同网络协议之间的转换&#xff0c;还在数据采集、设备控制、网络管理等方面展现出强大的功能。 一、协议转换功能 工业通信网关…

用Python打造媒体管理播放器:从零到全功能GUI应用

背景 在日常生活中&#xff0c;我们经常需要管理和播放大量媒体文件。市面上的音频播放器可能功能单一&#xff0c;或者界面复杂。作为一名程序员&#xff0c;我决定使用Python自己打造一个简单yet强大的媒体管理播放器。 C:\pythoncode\new\playsong.py 全部代码 import os…