科技,用来处理小半径邻域和子树,链等的结合题型。
本质是对树重编号,将询问的区间划分成尽可能少的区间。
BDFS 序:
这个可以处理同时存在邻域和子树查询的信息。
假设要求 \(k\) 邻域,那么单次操作复杂度就是 \(O(k \log n)\)。
重编号设计:
重编号思想:
容易发现 bfs 序满足子树任意的 \(k\) 邻域都是一个区间,而 \(dfs\) 序满足子树是一个区间。
此时我们不需要很多的 \(k\),但是要查一整颗子树。
考虑将二者结合,我们将原来的 \(dfs\) 序中, \(u\) 的位置替换为他的子树 \(k\) 邻域集合的 \(bfs\) 序。
此时满足所有 \(k' \le k\) 邻域都是一个区间,而 \(u\) 的所有后代替换的位置都在 \(u\) 之后,说明 \(k' >k\) 的邻域子树一起构成一个区间。
重编号流程:
假设最大要求查询 \(k\) 邻域。
-
先将根节点的 \(k-1\) 及以下邻域按照 bfs 序加入编号序列。
-
定义 \(son_{u,i}\) 表示 \(u\) 子树中 \(i\) 邻域的点的集合,按照 \(dfs\) 序排序。按 dfs 顺序访问到 \(u\) 的时候,依次将 \(son_{u,k}\) 中的元素加入编号序列。
点击查看代码
for(int i=0;i<M-1;i++) for(auto v:son[1][i]) dfn[v]=++ts;void Dfs(int u)
{Bg[u]=ts+1;for(auto v:son[u][M-1]) dfn[v]=++ts;for(auto v:e[u]){if(v==fa[u]) continue;Dfs(v);}En[u]=ts;
}
\([Bg_u, En_u]\) 是所有大于 \(k\) 的邻域子树编号区间。
询问流程
-
邻域操作:
枚举和邻域中的点的 \(Lca\),注意如果要求只算 \(k\) 邻域而不算更小的,需要一些容斥:
-
\(K\) 以内邻域:
点击查看代码
int Subk(int u,int k,int c) {return modify(1,L[u][k],R[u][k],c); }int calc1(int u,int k,int c) {int mx=-Inf;for(int i=k;i>=0;i--){mx=max(mx,Subk(u,i,c));if(fa[u] && i) mx=max(mx,Subk(u,i-1,c));if(fa[u]) u=fa[u];} return mx; }
-
\(K\) 邻域:
直接对整体容斥:
点击查看代码
int calc2(int u,int k,int c) {if(k) calc1(u,k-1,-Inf);int res=calc1(u,k,c);if(k) calc1(u,k-1,Inf-c);return res; }
这是其中一种写法,基于全局的容斥,还有一种是可以做不支持容斥的,直接对区间做容斥:
点击查看代码
int Calc1(int u,int k,int c) {int res=-Inf;modify(1,L[u][k],R[u][k],c,res);int d=1;while(fa[u] && d<=k){int p=fa[u];int _d=k-d;if(_d==0){modify(1,dfn[p],dfn[p],c,res); }else{int l1=L[p][_d],r1=R[p][_d],l2=L[u][_d-1],r2=R[u][_d-1];if(l2<=r2){modify(1,l1,l2-1,c,res),modify(1,r2+1,r1,c,res);}else{modify(1,l1,r1,c,res);} }u=p,d++;}return res; }
-
-
子树操作:
拆分为子树内 \(k\) 以内邻域以及 \(k\) 以外子树即可:
点击查看代码
int Sub(int u,int c) {int res=-Inf;for(int i=0;i<M;i++) res=max(res,Max(u,i,c));res=max(res,modify(1,L[u][M],R[u][M],c));return res; }
例题:
DMY 邻域查询
K-毛毛虫剖分
这个是上面 bdfs 序的一个树剖加强,增加了链邻域的操作,更为复杂。
本质是我们将树剖成若干重链,然后按照 dfs 的顺序去遍历这些链(不一定要是严格的,可以看成轻边链接的都是父子关系)。
然后每条链去做刚刚那个 bdfs 重编号。
此时我们注意到这样做满足如下性质:
-
每个点,除去重儿子方向的子树,满足 \(k\) 邻域重编号构成区间,\(k\) 以外子树内邻域重编号为区间。
-
对于每条重链,除去链顶端的 \(k\) 个点,其余所有点的 \(k\) 邻域重编号构成一个区间。
因此对于子树方向的查询相比之前,只需要多讨论重儿子即可。
对于链上的询问,相比树剖需要特殊考虑链顶的 \(k\) 个节点。
代码可以这样写:
点击查看代码
void renum(int u,int d,int p,int &l,int &r)//找到距离 u 恰好为 d 的点
{if(!d){if(!dfn[u]) dfn[u]=++ts,seq[ts]=u;l=min(l,dfn[u]),r=max(r,dfn[u]);return;}for(auto v:e[u]){if(v==fa[u] || (!p && v==son[u])) continue;renum(v,d-1,u,l,r);}
}void reorder(int u)
{int v;for(int i=0;i<=M;i++)//bfs序依次加入{v=u;while(v){int bg=ts;renum(v,i,0,L[v][i],R[v][i]);if(L[v][i]>R[v][i]) L[v][i]=bg+1,R[v][i]=bg;//如果为空,设置为 [last+1,last],保证和下面的构成连续区间v=son[v];}}v=u;while(v)//按dfs序处理重链{Bg[v]=ts+1;// k 以外邻域起始编号for(auto z:e[v]){if(z==fa[v] || z==son[v]) continue;reorder(z);}v=son[v];}v=u;while(v) En[v]=ts,v=son[v];
}
基础操作:
-
对于某个点 \(u\),它的子树 \(k\) 邻域操作:
每次将这一层的轻儿子整体做完后,递归到重儿子处理,由于 \(k\) 很小,次数不会超过 \(k\) 次,复杂度 \(O(k \log n)\)。
点击查看代码
void Suboperate(int u,int k) {for(int i=k;i>=0;i--){operate(L[u][i],R[u][i],c);u=son[u];} }
有了这个我们就可以较为方便的做某个点的 \(k\) 以内邻域操作了:
点击查看代码
void Neoperate(int u,int k) {for(int i=k;i>=0 ;i--){Suboperate(u,i);if(fa[u] && i) Suboperate(u,i-1);if(fa[u]) u=fa[u];} }
-
对于条重链片段 \(u \rightarrow v\),操作链方向子树内,链的 \(k\) 以内邻域,不包括连顶 \(k-1\) 内邻域。
为啥要不包含链顶?因为如果他的上面没有链了,说明到达了 LCA,直接补上即可。否则他的上面一定有链,在这个链执行完当前操作后就会把他的链顶部分给算上。
相当于所有点同时做上面的操作,每次只算恰好 \(k\) 邻域的部分,链顶前几个单独拎出来加,后面的 \(k\) 邻域轻儿子部分构成了一个完整区间。然后所有点向下平移,递归处理。单词递归复杂度 \(O(k \log n)\),总复杂度 \(O(k^2 \log n)\)。
点击查看代码
void Linkoperate(int u,int v,int k) {if(!u) return;if(k) Linkoperate(son[u],(son[v])?son[v]:v,k-1);for(int i=0;i<M && u!=v;i++){operate(L[u][k],R[u][k]);u=son[u];}operate(L[u][k],R[v][k]); }
进阶操作(子树,链邻域)
-
子树:
根据重编号的性质有:对于每个点,除去重儿子方向,所有 \(k\) 以外邻域,子树内部分构成一个完整区间。这部分直接做就行。
然后补上重儿子方向的,可以使用刚才的链操作维护。
这样最后还剩下 \(u\) 的 \(k-1\) 以内邻域,子树内部分,每一层做基础操作 1 就行。
总复杂度 \(O(\log n+ k^2 \log n+k^2 \log n)=O(k^2 \log n)\)。
点击查看代码
void operate_sub(int u) {operate(Bg[u],En[u]);Linkoperate(u,ed[u],M);//ed_u 是 u 所在重链的末尾 for(int i=0;i<M;i++) Suboperate(u,i); }
-
链邻域:
拆出来每条重链分别做,注意不要操作到 LCA。那这样最后就剩下了 \(u\) 的 \(k\) 邻域以内部分了。
点击查看代码
void operate_link(int u,int v,int k) {while(top[u]!=top[v]){if(dep[top[u]]<dep[top[v]]) swap(u,v);Linkoperate(top[u],u,k);u=fa[top[u]];}if(dep[u]>dep[v]) swap(u,v);if(u!=v) Linkoperate(son[u],v,k);Neoperate(u,k,c); }
例题
[2023集训队互测] 数据结构
[2024集训队互测] 数据结构
网上讲解相当少,而且大部分都是 1-毛毛虫剖分,只能自己看这两道例题别人的代码理解,非常困难,肝了一天才差不多明白,代码理解了之后其实并不难写。