冷门科技 —— DFS 序求解 LA

  • 该技巧目前已知的最早来源:我自己。

dfs 序求 k 级祖先无论是在实际效率,空间常数还是好写程度均吊打长链剖分。

定义

  1. dfs 序表示对一棵树进行深度优先搜索得到的结点序列。
  2. d i d_i di 为点 i i i 的深度,其中根的深度为 1 1 1

算法介绍

考虑现在我们要求节点 u u u 的第 k k k 级祖先,设答案为 f f f,则 d f d_f df 是已知的。

那我们现在在所有深度是 d f d_f df 的点中找到 u u u 的祖先即可。

显然, f f f u u u 的祖先当且仅当以 f f f 为根的子树包含以 u u u 为根的子树。

于是可以转为 dfs 序。

设点 x x x 的后代在 dfs 序上对应的区间为 [ l x , r x ] [l_x,r_x] [lx,rx],那么查询就是问在深度为 d f d_f df 对应的点对应的区间中,是 [ l u , r u ] [l_u,r_u] [lu,ru] 的超集的是哪个点,显然只会存在一个,二分秒了。

然后你发现可以将 l u l_u lu r u r_u ru 丢掉,只保留一个,仍然可以做。

但是保留 r u r_u ru 就可以直接用 lower_bound,所以我们选择保留 r u r_u ru

这是P5903 【模板】树上 K 级祖先的代码。

875 字节,厉不厉害?

#include<bits/stdc++.h>
using namespace std;
#define int long long
int n,q,dep[500005],cnt,r[500005];
int nxt[500005],head[500005];
vector<pair<int,int>>vec[500005];
unsigned s;
void add(int u,int v){
	nxt[v]=head[u];
	head[u]=v;
}
inline unsigned get(unsigned x){
	x^=x<<13;
	x^=x>>17;
	x^=x<<5;
	return s=x;
}
void dfs(int u){
	r[u]=++cnt;
	for(int v=head[u];v;v=nxt[v]){
		dep[v]=dep[u]+1;
		dfs(v);
		r[u]=r[v];
	}
	vec[dep[u]].push_back({r[u],u});
}
signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	cin>>n>>q>>s;
	int rt=0;
	for(int i=1;i<=n;i++){
		int f(0);
		cin>>f;
		rt+=(!f)*i;
		if(f)add(f,i);
	}
	dep[rt]=1;
	dfs(rt);
	int lst=0,sum=0;
	for(int i=1;i<=q;i++){
		int u=(get(s)^lst)%n+1,k=(get(s)^lst)%dep[u];
		lst=lower_bound(vec[dep[u]-k].begin(),vec[dep[u]-k].end(),make_pair(r[u],-1ll))->second;
		sum^=1ll*i*lst;
	}
	cout<<sum;
	return 0;
}

当然,这份代码显然不是跑得很快的,这份才是:

#include<bits/stdc++.h>
using namespace std;
int n,q,dep[500005],cnt,r[500005];
vector<int>g[500005];
vector<pair<int,int>>vec[500005];
unsigned s;
inline unsigned get(unsigned x){
	x^=x<<13;
	x^=x>>17;
	x^=x<<5;
	return s=x;
}
void dfs(int u){
	r[u]=++cnt;
	for(int v:g[u]){
		dep[v]=dep[u]+1;
		dfs(v);
		r[u]=max(r[u],r[v]);
	}
	vec[dep[u]].push_back({r[u],u});
}
void read(auto&x){
	char c(getchar());
	while(c<'0')c=getchar();
	while(c>='0')x=(x<<1)+(x<<3)+(c^48),c=getchar();
}
signed main(){
	read(n),read(q),read(s);
	int rt=0;
	for(int i=1;i<=n;i++){
		int f(0);
		read(f);
		rt+=(!f)*i;
		g[f].push_back(i);
	}
	dep[rt]=1;
	dfs(rt);
	int lst=0;
	long long sum=0;
	for(int i=1;i<=q;i++){
		int u=(get(s)^lst)%n+1,k=(get(s)^lst)%dep[u];
		lst=lower_bound(vec[dep[u]-k].begin(),vec[dep[u]-k].end(),make_pair(r[u],-1))->second;
		sum^=1ll*i*lst;
	}
	cout<<sum;
	return 0;
}

和各种 LA 算法的对比

对比 DFS 序和重链剖分,预处理的时间常数减半(重链剖分需要两次 dfs),而且更好写,但两种算法表现都不错,因此树剖 LA 也是不错的选择。

对比 DFS 序和倍增,前者预处理复杂度更优,查询常数也更小。

对比 DFS 序和长链剖分,前者预处理复杂度更优,且实际效率也更高(访问连续性)。

对于其它算法,请读者自行比较。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值