树链剖分详解(长链剖分)

参考资料

例题:P5903 树上 \(k\) 级祖先

例题:CF1009F Dominant Indices

因为当时写树链剖分详解(重链剖分)时并不会长链剖分,所以只能另开一文了。


长链剖分」,除「重链剖分」外的另一种树剖方式,与深度相关。

长链剖分

定义「重子节点」表示子树深度最大的子节点,「轻子节点」表示剩余子节点。其余定义同「重链剖分」的定义。(但是也有人称之为「长子节点」和「短子节点」)

长链剖分可以容易地类似于重链剖分实现,只需要在寻找重子节点时稍作修改即可。

性质

没什么用的性质。

从任意节点出发往上跳,跳轻边的次数为 \(\mathcal O\left(\sqrt n\right)\)。也就是说,长链数量为 \(\mathcal O\left(\sqrt n\right)\)

也就是说,使用长链剖分可以得到一个单次操作复杂度 \(\mathcal O\left(\sqrt n\log n\right)\) 的算法。

但是长链剖分的实际应用并不是代替重链剖分,而是维护一些与「深度」相关的信息。

简单应用

CF208E Blood Cousins

给定一棵树,和 \(m\) 次询问,每次求一个节点 \(v\)\(p\) 级祖先除 \(v\) 外有多少个 \(p\) 级子节点。

给定一棵树,确定每一个点 \(x\) 的子树内各深度的子节点数量。要求时间复杂度为线性。(重链剖分配合 DSU on tree可以做到 \(\mathcal O(n\log n)\)

维护信息与深度正相关,点 \(x\) 的信息量为 \(\mathcal O(\textit{depth}_x)\),考虑长链剖分。

\(x\) 可以 \(\mathcal O(1)\) 继承重子节点的信息,其余轻子节点 \(i\)\(\mathcal O(\textit{depth}_i)\) 的代价暴力继承信息即可。

此时时间复杂度为 \(\mathcal O(n)\)。因为每一条长链都只会被统计一次。

维护诸如此类的深度相关信息,均可以类似地做到 \(\mathcal O(n)\)

长链剖分求解树上 \(k\) 级祖先

例题:P5903 树上 \(k\) 级祖先


给定一棵 \(n\) 个点的有根树,\(q\) 次询问,每次询问点 \(x\)\(k\) 级祖先。

树上 \(k\) 级祖先可以通过倍增做到 \(\mathcal O(\log n)\) 查询,通过重链剖分配合预处理做到 \(\mathcal O(\log\log n)\),重链剖分配合分块做到 \(\mathcal O\left(\sqrt{\log n}\right)\)

但是长链剖分可以做到 \(\mathcal O(n\log n)\) 预处理,\(\mathcal O(1)\) 查询。

首先可以倍增预处理节点的 \(2^i\) 级祖先。

\(x\)\(k\) 级祖先,找到一个 \(2^i\leq k<2^{i+1}\),跳到 \(x\)\(2^i\) 级祖先,令 \(k\leftarrow k-2^i\)

根据长链剖分的性质,此时有 \(x\) 所在链的长度大于等于 \(2^i\),进而大于等于 \(k\)。因此可以预处理链顶节点的 \(l\) 级祖先和 \(l\) 级重子节点,\(l\) 表示链的长度。在预处理出来的结果上查询即可。

证明

注意到:如果 $x$ 所在链链顶节点 $\textit{top}_x$ 的父节点 $y$ 所在链长度一定大于 $x$ 所在链长度。

否则,$x,y$ 将会处于同一条链中。

若 $x$ 的 $k$ 级祖先处于同一条链中,正确性显然。

否则若处于链顶节点的父节点所处链中,则必然 $k$ 被划分为了两部分。父节点所处链的长度大于当前链长度,因此正确。

可以推广到跳了任意条。

注意特判 \(k=0\)

参考代码
//#include<bits/stdc++.h>
#include<algorithm>
#include<iostream>
#include<cstring>
#include<iomanip>
#include<cstdio>
#include<string>
#include<vector>
#include<cmath>
#include<ctime>
#include<deque>
#include<queue>
#include<stack>
#include<list>
using namespace std;
typedef long long ll;
constexpr const int N=5e5;
namespace judge{
	#define ui unsigned int
	ui s;
	
	inline ui get(ui x) {
		x ^= x << 13;
		x ^= x >> 17;
		x ^= x << 5;
		return s = x; 
	}
	#undef ui
}
int n,root,depth[N+1],father[N+1][__lg(N+1)+1];
vector<int>g[N+1];
namespace hld{
	int son[N+1],maxDepth[N+1],top[N+1],len[N+1];
	void dfs1(int x){
		depth[x]=depth[father[x][0]]+1;
		maxDepth[x]=depth[x];
		for(int i:g[x]){
			if(i==father[x][0]){
				continue;
			}
			dfs1(i);
			maxDepth[x]=max(maxDepth[x],maxDepth[i]);
			if(maxDepth[i]>maxDepth[son[x]]){
				son[x]=i;
			}
		}
	}
	vector<int>up[N+1],down[N+1];
	void dfs2(int x,int topx){ 
		top[x]=topx;
		if(son[x]){
			dfs2(son[x],topx);
		}
		for(int i:g[x]){
			if(i==father[x][0]||i==son[x]){
				continue;
			}
			dfs2(i,i);
		}
		if(x==topx){
			for(int y=x;son[y];y=son[y]){
				down[x].push_back(y);
			}
			for(int i=1,y=x;i<=down[x].size();i++,y=father[y][0]){
				up[x].push_back(y);
			}
		}
	}
	void pre(){
		dfs1(root);
		dfs2(root,root);
		for(int i=1;(1<<i)<=n;i++){
			for(int x=1;x<=n;x++){
				father[x][i]=father[father[x][i-1]][i-1];
			}
		}
	}
	int query(int x,int k){
		if(!k){
			return x;
		}
		x=father[x][__lg(k)];
		k-=1<<__lg(k);
		k-=depth[x]-depth[top[x]];
		x=top[x];
		if(k>=0){
			return up[x][k];
		}else{
			return down[x][-k];
		}
	}
}
int main(){
	/*freopen("test.in","r",stdin);
	freopen("test.out","w",stdout);*/
	
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	
	int q;
	cin>>n>>q>>judge::s;
	for(int i=1;i<=n;i++){
		cin>>father[i][0];
		if(!father[i][0]){
			root=i;
		}else{
			g[father[i][0]].push_back(i);
		}
	}
	hld::pre();
	ll ans=0,last=0;
	for(int i=1;i<=q;i++){
		int x=(judge::get(judge::s)^last)%n+1;
		int k=(judge::get(judge::s)^last)%depth[x];
		last=hld::query(x,k);
		ans^=1ll*i*last;
	}
	cout<<ans<<'\n';
	
	cout.flush();
	
	/*fclose(stdin);
	fclose(stdout);*/
	return 0;
}

长链剖分优化 DP

例题:CF1009F Dominant Indices


给定一棵以 \(1\) 作为根的 \(n\) 个点的有根树。

定义 \(d_{x,k}\) 表示 \(x\)\(k\) 级子节点的数量。对于每一个点 \(x\),求最小的 \(k\) 满足 \(d_{x,k}\) 最大。钦定 \(d_{x,0}=1\)

\(v_x\) 表示 \(x\) 的子节点集,有:

\[d_{x,k}=\sum_{i\in v_x}d_{x,k-1} \]

暴力维护会 TLE 并且 MLE。注意到 DP 是与深度相关的,考虑长链剖分。

考虑如何 \(\mathcal O(1)\) 继承重子节点信息。可以采用指针。具体而言,将 \(x\) 的重子节点 \(\textit{son}_x\) 的信息 \(d_{\textit{son}_x,0},d_{\textit{son}_x,1},\cdots,d_{\textit{son}_x,k}\) 直接映射\(d_{x,1},d_{x,2},\cdots,d_{x,k+1}\) 上即可。

这样,每一条长链对应一组存储的 \(d\),总空间复杂度为 \(\mathcal O(n)\)。时间复杂度也为 \(\mathcal O(n)\)

同时需要时刻维护当前序列 \(d\) 的最大值及其下标。

参考代码
//#include<bits/stdc++.h>
#include<algorithm>
#include<iostream>
#include<cstring>
#include<iomanip>
#include<cstdio>
#include<string>
#include<vector>
#include<cmath>
#include<ctime>
#include<deque>
#include<queue>
#include<stack>
#include<list>
using namespace std;
constexpr const int N=1e6;
int n,ans[N+1];
vector<int>g[N+1];
struct d{
	int max,id;
	vector<int>a;
	d(){
		max=1;
		id=0;
	} 
	int size(){
		return a.size();
	}
	void resize(int x){
		a.resize(x);
	}
	int& operator [](int x){
		return a[x];
	}
}d[N+1];
namespace hld{
	int depth[N+1],maxDepth[N+1],son[N+1];
	void dfs1(int x,int fx){
		depth[x]=depth[fx]+1;
		maxDepth[x]=depth[x];
		for(int i:g[x]){
			if(i==fx){
				continue;
			}
			dfs1(i,x);
			maxDepth[x]=max(maxDepth[x],maxDepth[i]);
			if(maxDepth[i]>maxDepth[son[x]]){
				son[x]=i;
			}
		}
	}
	int cnt; 
	void dfs2(int x,int fx,int id,int pre){
		if(d[id].size()<pre+1){
			d[id].resize(pre+1);
		}
		d[id][pre]=1;
		if(son[x]){
			dfs2(son[x],x,id,pre+1);
		}
		for(int i:g[x]){
			if(i==son[x]||i==fx){
				continue;
			}
			int id2=++cnt;
			dfs2(i,x,id2,0);
			if(d[id].size()<pre+1+d[id2].size()){
				d[id].resize(pre+1+d[id2].size());
			}
			for(int j=0;j<d[id2].size();j++){
				d[id][pre+1+j]+=d[id2][j];
				if(d[id][pre+1+j]>d[id].max){
					d[id].max=d[id][pre+1+j];
					d[id].id=pre+1+j;
				}else if(d[id][pre+1+j]==d[id].max){
					d[id].id=min(d[id].id,pre+1+j);
				}
			}
		}
		ans[x]=max(d[id].id-pre,0);//若小于 0,则说明为 pre。
	}
	int main(){
		dfs1(1,0);
		cnt=1;
		dfs2(1,0,1,0);
		return 0;
	}
}
int main(){
	/*freopen("test.in","r",stdin);
	freopen("test.out","w",stdout);*/
	
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	
	cin>>n;
	for(int i=1;i<n;i++){
		int u,v;
		cin>>u>>v;
		g[u].push_back(v);
		g[v].push_back(u);
	}
	hld::main();
	for(int i=1;i<=n;i++){
		cout<<ans[i]<<'\n';
	}
	
	cout.flush();
	 
	/*fclose(stdin);
	fclose(stdout);*/
	return 0;
}
posted @ 2025-08-14 00:20  TH911  阅读(47)  评论(0)    收藏  举报