关于一类树上路径操作的通用做法(毛毛虫剖分)
问法:
每次给出一条路径 \(S=(u, v)\),对所有 \(x\in S\) 的 \(x\) 进行某种操作,对所有 \(x \in S, y\notin S, (x, y) \in Tree\) 进行另外一种操作。
简单来说就是路径操作涉及到所有路径上点的子节点。
经 xgf 鸽鸽 提醒,这玩意有个正式名字:毛毛虫剖分
通解(?
考虑我们重链剖分拍出的 dfn 序,它满足两个条件:
-
每条重链上的所有点编号是连续的
-
子树内的所有点编号是连续段
发现 [子树] 这个信息在操作并不重要,我们尝试将其改为:
-
每条重链上的所有点编号是连续的
-
每条重链上的所有的儿子(除去原本重链上的点)的编号也连续
这看起来是不可能的,考虑退一步,在满足第二条件时,尽量满足条件一。
根据参考资料不难得出一个简单的构造方法:
初始:把树根的重链塞入队列。
循环直到队空
-
取出队头的重链 \(Z\)
-
依次给 \(Z\) 上的点编号
-
依次给 \(Z\) 上的每个点的儿子编号(已经有的就不给),这些点必然是一条重链的 top(显然不包括 \(Z\) 中的点),将他们加进队列。
美中不足的是,重链 \(Z\) 的 \(top\) 和 \(Z\) 中的其他点可能分离,这个可以分讨解决。
要不,我们再添点限制?
-
每条重链上的所有点编号是连续的
-
每条重链上的所有的儿子(除去原本重链上的点)的编号也连续
-
子树内的所有点编号是连续段
还是考虑尽量满足,这次标号换成递归式:
设现在递归到 x,先给 x 标号
若 x 是所在重链链顶,将 \(Z_{x}\) 的所有轻儿子标号
递归重儿子,然后递归轻儿子
void cov(int x){
cat[x][0]=p+1;for(auto v:G[x]) if(dep[v]>dep[x]&&(v^son[x])) dfn[v]=++p;
cat[x][1]=p;if(son[x]) cov(son[x]);
}
void dfs(int x, int rt){
edp[top[x]=rt]=x;if(!dfn[x]) dfn[x]=++p;if(x==top[x]) cov(x);sub[x][0]=p+1;
if(son[x]) dfs(son[x], rt);for(auto v:G[x]) if(!top[v]) dfs(v, v);sub[x][1]=p;
}
这样子能做到
- 一条重链为 \([dfn_{rt}, dfn_{rt}]\) and \([dfn_{son_{rt}}, dfn_{edp_{rt}}]\) 且 rt 的 dfn 序最小
- \([sub_{x, 0}, sub_{x, 1}]]\) 为子树中恰好除去了根重链所在毛毛虫脚的部分
- 一条重链部分 \((x, y)\) 的毛毛虫脚部分为 \([cat_{x, 0}, cat_{y, 1}]\)
例题:
NOI2021 轻重边,直接去看参考资料(
2022.09.14 SS 集训 T3 洛可可天使
每个点维护到父亲的边的边权的期望值,每次操作其实就是 \(1-x\) 路径上的点权 \(\times (1-p_i)+p_i\times c_i\)(有 \((1-p_i)\) 的概率不变,剩下有 \(p_i\) 的概率变为 \(c_i\)),以及这条链所有的旁系儿子点权 \(\times (1-p_i)\)。
考虑按照上面的做法拍成 dfs 序,直接暴力线段树即可。
注意向上跳的过程中,即将跳的上面的链的底部重儿子是不会被算入旁系儿子的,要单独乘一次,我当前链的顶部可能会在上面被当成旁系儿子,所以记得乘加的时候跳过这个点(裂开成两个区间)。
复杂度 \(O(m\log^2n)\)
P8479 「GLR-R3」谷雨
对于最大子段和这一类需要考虑顺序合并的,可能要建立两种剖分序。详细题解
扩展 k-邻域 毛毛虫 更通的解(?
单点 k-邻域
重剖 dfs 序的好处在于处理 1-邻域的时候能同时处理子树,若单纯考虑 \(k\)-邻域(\(k\) 为小常数),我们来考虑性质更契合的 bfs 序与 dfs 序结合。
参考 1-邻域 的设计,具体地,给出如下构造法:
- 将根节点的 \([1, k)\)-邻域按 bfs 序加入 dfn 序
- 按 dfs 序遍历每个点,将 \(k\)-邻域的点 按 bfs 序加入 dfn 序
这样就能做到查询?别急,让我们检查一下有什么问题。
对于某个点 \(x\),若它是 根 的 \([1, k)\)-邻域(在树上等价于 \(dep_x<k\)):
它的 \([1, k-dep_{x}]\)-邻域 在一开始按 bfs 序加入的点中,那么对于 \(i\in [1, k-dep_x]\),\(x\) 往下的 \(i\)-邻域 的点在这里面一定是连续区间。而 \(k\)-邻域 则在 dfs 序遍历到自己时加入。
对于 \(i\in (k-dep_x, k)\)-邻域,考虑其 \(i-k+dep_x\) 辈祖先 \(y\),\(x\) 的 \(i\)-邻域 对于 \(y\) 来说是 \(k\)-邻域,同样也是连续区间。
你很有可能已经发现,对于不是根 \([1, k)\)-邻域 里面的点,其任意邻域的点在其祖先的 \(k\)-邻域 被加入。
代码实现部分:
预处理
考虑到可能询问 \(k\le K\) 的邻域,我们记录 \(L_{x, k}, R_{x, k}\) 表示 \(x\) 子树中 \(k\) 邻域的编号,注意到对于 \(k<K\) 的邻域,\(x\) 的 \(k\) 邻域会在 \(x\) 的 \(K-k\) 级祖先加入,故也是一个连续区间。具体的,我们在重编号时,每遍历到一个点,bfs 其 \(K\) 以内邻域的所有点进行编号,同时统计 \(L_{x, k}, R_{x, k}\)。
void dfs(int x){
siz[x]=1;
for(auto v:G[x])
if(!dep[v]){
dep[v]=dep[x]+1, fa[v]=x;dfs(v);siz[x]+=siz[v];
if(siz[v]>siz[son[x]]) son[x]=v;
}
}
void bfs(int x){
queue<int> q;q.push(x);
for(int i=0; i<=K; ++i) L[x][k]=n+1, R[x][k]=0;
while(!q.empty()){
int y=q.front(), dp=dep[y]-dep[x];q.pop();
if(!dfn[y]) dfn[y]=++idc, bac[idc]=y;
L[x][dp]=min(L[x][dp], dfn[y]);
R[x][dp]=max(R[x][dp], dfn[y]);
if(dp<K) for(auto v:G[y]) if(fa[v]^y) q.push(v);
}
}
void renb(int x){
bfs(x);st[x]=idc+1;
for(auto v:G[x]) if(fa[v]^x) renb(v);
ed[x]=idc;
}
单点子树内恰 K-邻域 或 K 以内邻域 操作
直接调用 \(L_{x, k}, R_{x, k}\) 进行区间操作和查询即可。
inline ds Kth(int x, int d, int v, bool opt){
ds res=inf;
if(!opt) Seg::mdy(1, 1, n, L[x][d], R[x][d], v);
else res+=Seg::qry(1, 1, n, L[x][d], R[x][d]);
return res;
}
那么 K-以内邻域就是枚举 \(d\) 分别算每一层,复杂度 \(O(K\operatorname{ds}(n))\)。
单点 K-以内邻域查询
设询问点 \(x\) K-以内邻域的块状物为 \(\operatorname{U}_{x, k}\),考虑每一个 \(y\in \operatorname{U}_{x, k}\)。
直接想法是在 \(\operatorname{LCA(x, y)}\) 计算 \(y\),那么枚举 \(x\) 的 \(i\) 级祖先 \(fa_{x, i}\)(\(i\in[0, K]\)),枚举 \(k'=\operatorname{dist}(fa_{x, i}, y)\le K-i\),显然合法的 \(dfn_y\) 为 \([L_{fa_{x, i}, k'}, R_{fa_{x, i}, k'}]\backslash [L_{fa_{x, i-1}, k'-1}, R_{fa_{x, i-1}, k'-1}]\),复杂度 \(O(K^2\operatorname{ds}(n))\)。
这个想法十分基础但复杂度不优,且由于容斥导致扩展性受限,我们考虑降低一个 \(K\),逐层计算。
.png)
考虑 9 这个点的 3-以内邻域,枚举 \(K-i\) 为向上走了几步,那么剩下还剩 \(i\) 步,我们希望一次性把所有深度相同的 \(y\) 都算了,那么直接算 \([L_{fa_{x, K-i}, i}, R_{fa_{x, K-i}, i}]\) 就行了?
发现我们会少算 12 这个点,为什么?因为我们发现,从某个祖先往 \(x\) 所在子树走的代价是 -1,但在父亲位置算,这条边的代价是 1,所以对于每个 \(fa_{x, i}\) 若其有父亲,则需额外算 \([L_{fa_{x, K-i}, i-1}, R_{fa_{x, K-i}, i-1}]\)。
void KthMdy(int x, int d, int v){
for(int i=d; i>=0; --i){
Mdy(x, i, v);if(i&&fa[x]) Mdy(x, i-1, v);
if(fa[x]) x=fa[x];
}
}
ull KthQry(int x, int d){
ull res=0;
for(int i=d; i>=0; --i){
res+=Qry(x, i);
if(i&&fa[x]) res+=Qry(x, i-1);
if(fa[x]) x=fa[x];
}return res;
}
单点子树内查询
注意到按上面的方式,对于 \(x\),其子树内除 K-以内邻域 里的点,都要在遍历 \(x\) 后才得到编号,故完全连续。那么处理 \(st_{x}, ed_x\) 表示剩下部分,就可以做子树操作了。
路径 K-邻域 & 重(chong)链剖分
我们不满足于单点,希望类似 1-邻域 重构树链剖分完成链上的操作。
同样考虑先给某条重链的 K-以内邻域 标号,具体地,由于我们希望每条重链的每层邻域都是连续的一部分,所以我们枚举层数 \(k\),同样重复上面的编号操作,遍历重链每一个点都进行一次深度为 \(k\) 的子树 k-邻域标号。
void bfs(int x, int d){
L[x][d]=n+1, R[x][d]=0;
queue<int> q;q.push(x);
while(!q.empty()){
int y=q.front(), dp=dep[y]-dep[x];q.pop();
if(!dfn[y]) dfn[y]=++idc, bac[idc]=y;
if(dp==d) L[x][dp]=min(L[x][dp], dfn[y]), R[x][dp]=max(R[x][dp], dfn[y]);
else{
for(auto v:G[y]) if(fa[y]!=v&&son[y]!=v) q.push(v);
if(y!=x&&son[y]) q.push(son[y]);
}
}
}
void renb(int x){
for(int i=0, t; i<=K; ++i)
for(int u=x; u; u=son[u]){
t=idc;bfs(u, i);
if(L[u][i]>R[u][i]) L[u][i]=t+1, R[u][i]=t;
}
for(int u=x; u; u=son[u]){
top[u]=x, ced[x]=u;st[u]=idc+1;
for(auto v:G[u]) if(fa[u]!=v&&son[u]!=v) renb(v);//遍历重链的所有旁支轻边走其他链
}
for(int u=x; u; u=son[u]) ed[u]=idc, ced[u]=ced[x];
}
对于第 18 行的解释会在后面说明。
如此操作,我们能让每条重链在链上所有点的 K-以内邻域(毛毛虫状物)中,除(链顶 \(x\) 父亲所覆盖的 K-以内邻域 中)在该重链上的点外,其他点都能按相对深度(即到链的距离)带权排序后,毛毛虫状物中每个深度恰好为一个连续区间。
单点 \(x\) 子树内恰 K-邻域 操作
不断往重儿子跳,查询对应邻域即可。
为什么不在 \(x\) 直接询问而还要往下询问?注意到我们只能保证,对于某个重链的毛毛虫状物内的点,到重链距离相等的点标号连续,所以如果只查 \([L_{x, k}, R_{x, k}]\) 的话,\([L_{son_x, k-1}, R_{son_x, k-1}]\) 是查询不到的。
inline ds Kth(int x, int d, int v, bool opt){
ds res=inf;
if(!opt) Seg::mdy(1, 1, n, L[x][d], R[x][d], v);
else res+=Seg::qry(1, 1, n, L[x][d], R[x][d]);
return res;
}
ds Sub_Kth(int x, int d, int v, bool opt){//子树K-邻域
ds res=inf;
for(int i=d; i>=0&&x; --i, x=son[x]) res+=Kth(x, i, v, opt);
return res;
}
为了支持链修改,这里的复杂度从 \(O(\operatorname{ds}(n))\) 摊成了 \(O(K\operatorname{ds}(n))\)。
单点 \(x\) 子树内 K-以内邻域 操作
同理,直接分别枚举 \(k\le K\) 即可。
单点 \(x\) K-以内邻域 操作
同理,向上跳逐层计算。
ds Pnt_leK(int x, int d, int v, bool opt){//单点K以内邻域
ds res=inf;
for(int i=d; i>=0; --i){
res+=Sub_Kth(x, i, v, opt);
if(fa[x]&&i) res+=Sub_Kth(x, i-1, v, opt);
if(fa[x]) x=fa[x];
}return res;
}
子树整体 与 路径 K-以内邻域 操作
已知 \([st_x, ed_x]\) 存储了子树小根 \(x\) 子树中,除 \(x\) 所在重链的 K-邻域毛毛虫状物,的所有点,这一部分是一个连续的大区间,直接查询。
考虑剩余部分,即那个毛毛虫状物,直观想法是枚举 \(k\),查询 \([L_{x, k}, R_{ced_x, k}]\),其中 \(ced_x\) 是重链底。
对于路径 K-以内邻域查询,我们拆成若干条重链进行操作,这时候就需要考虑查重,于是我们钦定:询问某条重链的部分 \([u, v](dep_u<dep_v)\) 时,不计算 \(u\) 处 \(k-1\)-以内邻域的点。
这样设置避免 \(fa_u\) 所在重链操作邻域时重复查询。
有限制的 重链片段 K-以内邻域 操作
既然不能设计 \(u\) 处 \(k-1\)-邻域以内的点,那么我们先只计算恰 K-邻域的点,理论上可以直接计算 \([L_{u, k}, R_{v, k}]\),但如上面所言:
如此操作,我们能让每条重链在链上所有点的 K-以内邻域(毛毛虫状物)中,除(链顶 \(x\) 父亲所覆盖的 K-以内邻域 中)在该重链上的点外,其他点都能按相对深度(即到链的距离)带权排序后,毛毛虫状物中每个深度恰好为一个连续区间。
所以考虑这种情况,防止分类讨论,我们单独把重链上面前 \(K\) 个点单独取出来算。
这时手动模拟我们会发现,我们把毛毛虫最外围一圈的点算了,剩下部分直接递归下去就行。
总复杂度 \(O(K^2\operatorname{ds}(n))\)
ds Chain_leK(int x, int y, int d, int v, bool opt){
//x, y 为同一重链先后两个点,不操作x子树外 K以内邻域
if(!x) return inf;ds res=inf;
if(d) res+=Chain_leK(son[x], (son[y])?son[y]:y, d-1, v, opt);
for(int i=0; i<K&&x!=y; ++i, x=son[x]) res+=Kth(x, d, v, opt);
if(!opt) Seg::mdy(1, 1, n, L[x][d], R[y][d], v);
else res+=Seg::qry(1, 1, n, L[x][d], R[y][d]);
return res;
}
子树整体 操作
在上面部分我们已经简单说明了子树操作,但为了合理利用函数,我们考虑直接利用 \(\operatorname{Chain_leK}(x, ced_x, k)\) 算掉一部分,这时候,回想起我们钦定的条件:
询问某条重链的部分 \([u, v](dep_u<dep_v)\) 时,不计算 \(u\) 处 \(k-1\)-以内邻域的点。
再把这部分补上就好。
ds Sub_all(int x, int v, bool opt){
ds res=Chain_leK(x, ced[x], K, v, opt);
if(!opt) Seg::mdy(1, 1, n, st[x], ed[x], v);
else res+=Seg::qry(1, 1, n, st[x], ed[x]);
for(int i=0; i<K; ++i) res+=Sub_Kth(x, i, v, opt);
return res;
}
路径 K-以内邻域 操作
模拟求 LCA 不断跳重链处理即可,为了好操作,我们把 LCA 留成一个单点进行 Pnt_leK 操作。
inline int LCA(int x, int y){
for(; top[x]^top[y]; x=fa[top[x]])
if(dep[top[x]]<dep[top[y]]) swap(x, y);
return dep[x]<dep[y]?x:y;
}
ds Route_leK(int x, int y, int d, int v, bool opt){
ds res=inf;int lca=LCA(x, y);
auto func=[&](int u, ds r=inf) -> ds{
for(; top[u]^top[lca]; u=fa[top[u]]) r+=Chain_leK(top[u], u, d, v, opt);
if(u!=lca) r+=Chain_leK(son[lca], u, d, v, opt);return r;
};res+=func(x)+func(y);
return res+Pnt_leK(lca, d, v, opt);
}
其余
其实还有路径恰 K-邻域部分,这段比较麻烦,初步想法是取消 Chain_leK 的递归,再外加分类讨论。
完整代码可以参考这个。
参考资料:
https://www.luogu.com.cn/blog/cyh-toby/solution-p7735
https://www.cnblogs.com/A-Quark/p/16435243.html
https://www.cnblogs.com/Richardwhr/p/18768685
https://blog.csdn.net/weixin_55851276/article/details/147128237
你发现了,其实是复读一遍参考资料

浙公网安备 33010602011771号