毛毛虫剖分
科技,用来处理小半径邻域和子树,链等的结合题型。
本质是对树重编号,将询问的区间划分成尽可能少的区间。
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; }
例题:
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); }
例题
网上讲解相当少,而且大部分都是 1-毛毛虫剖分,只能自己看这两道例题别人的代码理解,非常困难,肝了一天才差不多明白,代码理解了之后其实并不难写。

浙公网安备 33010602011771号