W
e
l
c
o
m
e
: )

关于一类树上路径操作的通用做法(毛毛虫剖分)

问法:

每次给出一条路径 \(S=(u, v)\),对所有 \(x\in S\)\(x\) 进行某种操作,对所有 \(x \in S, y\notin S, (x, y) \in Tree\) 进行另外一种操作。

简单来说就是路径操作涉及到所有路径上点的子节点。

xgf 鸽鸽 提醒,这玩意有个正式名字:毛毛虫剖分

通解(?

考虑我们重链剖分拍出的 dfn 序,它满足两个条件:

  • 每条重链上的所有点编号是连续的

  • 子树内的所有点编号是连续段

发现 [子树] 这个信息在操作并不重要,我们尝试将其改为:

  • 每条重链上的所有点编号是连续的

  • 每条重链上的所有的儿子(除去原本重链上的点)的编号也连续

这看起来是不可能的,考虑退一步,在满足第二条件时,尽量满足条件一。

根据参考资料不难得出一个简单的构造方法:

初始:把树根的重链塞入队列。

循环直到队空

  1. 取出队头的重链 \(Z\)

  2. 依次给 \(Z\) 上的点编号

  3. 依次给 \(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;
}

这样子能做到

  1. 一条重链为 \([dfn_{rt}, dfn_{rt}]\) and \([dfn_{son_{rt}}, dfn_{edp_{rt}}]\) 且 rt 的 dfn 序最小
  2. \([sub_{x, 0}, sub_{x, 1}]]\) 为子树中恰好除去了根重链所在毛毛虫脚的部分
  3. 一条重链部分 \((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. 将根节点的 \([1, k)\)-邻域按 bfs 序加入 dfn 序
  2. 按 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\),逐层计算。

考虑 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

你发现了,其实是复读一遍参考资料

posted @ 2022-09-14 16:18  127_127_127  阅读(547)  评论(1)    收藏  举报