[DS小计] 长链剖分

在周周转转几周被 DP 和 数论拷打后,滚回来学 DS 了。

什么是长剖

我们知道,我们学过重链剖分,它强大的性质使它处理链上问题十分顺手。
我们学过虚实链剖分,在处理连边断边链问题也很厉害。
长剖是树剖大家族的一员,与重剖很相似。

不同之处在于,重剖按的是子树大小,长剖按的是深度

好了,现在你已经知道长剖的实现方法了,和重剖一致,我们看看它有什么好的性质。

性质

  • \(0.\) 任意节点 \(x\) 的祖先所在长链长度大于等于 \(x\) 所在长链长度
    长剖就是这样剖的,每经过一条轻边长链长度至少减少 \(1\)
  • \(1.\) 从任一点到根节点切换轻重边次数为 \(\sqrt n\)
    证明:从第一次的重边跳到第二次的重边,重链长度至少加 \(1\),最坏是 \(1,2,3...,n\) 的。
    注意:重链长度指的是一整条重链的长度

这条性质告诉我们,长剖跳轻重边是很鸡肋的东西。

  • \(2.\) 任意节点的 \(k\) 级祖先长链长度大于等于 \(k\)
    证明:\(k\) 级祖先到这个节点这条链长度就是 \(k\) 了。

应用

\(1.\) \(O(n\log n)-O(1)\) 求解 \(k\) 级祖先问题

我们知道树上 \(k\) 级祖先用树剖、倍增可以做到 \(O(n\log n)-O(\log n)\) 的优秀复杂度。
如果能离线还可以 \(O(n)-O(1)\)

但是还是不够优秀。
长剖的复杂度是更优秀的 (听说还有 \(O(n)-O(1)\) 在线的?)
我们看看性质 \(2\)
假设我们处理出了 \(x\)\(2^i\) 祖先。
现在我们求 \(k\) 级祖先,先求出 \(2^i\le k<2^{i+1}\),然后往上跳 \(2^i\) 步。
根据性质,这条长链的长度 \(\ge 2^i\),而 \(k<2^i\),若重链长度为 \(d\) ,所以往上跳 \(k\) 步只有两种情况:

  • \(k\) 级祖先在重链上。
  • \(k\) 级祖先在重链 \(d\) 级祖先内。

我们可以预处理两个表,分别是从当前节点往上跳 \(d\) 格和往下 \(d\) 格的节点。
我们知道,所有链长度和为 \(n\),所以空间复杂度是线性的。(但是倍增空间复杂度是 \(O(n\log n)...\)

然后 vector 效率太shit了,我们知道,往下跳是很 EZ 的问题,dfn 是连续的,但是往上不好搞。那我们就用往下的点 \(x\) 存往上跳的点不就行了!

注意,这个码量十分惊人.....实战写树剖最好,常数小。

#include<bits/stdc++.h>
#define ll long long
#define N 500005
#define ui unsigned int
using namespace std;
ui s;
inline ui get(ui x) {
	x ^= x << 13;
	x ^= x >> 17;
	x ^= x << 5;
	return s = x; 
}
int n,q;
int fa[N];
int root;
int head[N],tot=1;
struct edge{
	int to,next;
}e[N];
void add(int u,int v)
{
	e[tot]=(edge){v,head[u]};
	head[u]=tot++;
}
int dep[N],Son[N],h[N];
int f[N][20];
void dfs1(int now)
{
	f[now][0]=fa[now];
	for(int i=1;f[now][i-1];i++)
		f[now][i]=f[f[now][i-1]][i-1];
	dep[now]=dep[fa[now]]+1;
	for(int i=head[now];i;i=e[i].next)
	{
		int son=e[i].to;
		dfs1(son);
		if(h[son]>h[now]) h[now]=h[son],Son[now]=son;
	}
	++h[now];
}
int top[N],D[N],U[N],tim,t[N];
void dfs2(int now,int topf,int cur)
{
	D[++tim]=now;
	U[tim]=cur;
	t[now]=tim;
	top[now]=topf;
	if(!Son[now]) return;
	dfs2(Son[now],topf,fa[cur]);
	for(int i=head[now];i;i=e[i].next)
	{
		int son=e[i].to;
		if(son==Son[now]) continue;
		dfs2(son,son,son);
	}
}
int last;
ll ans;
int ask(int x,int k)
{
	if(!k) return x;
	int len=__lg(k);
	x=f[x][len],k-=(1<<len);
	k=k-dep[x]+dep[top[x]];
	x=top[x];
	if(k>0) return U[t[x]+k];
	else return D[t[x]-k];
}
int main()
{
	scanf("%d%d%u",&n,&q,&s);
	for(int i=1;i<=n;i++)
	{
		scanf("%d",&fa[i]);
		if(!fa[i]) root=i;
		add(fa[i],i);
	}
	dfs1(root);
	dfs2(root,root,root);
	for(int i=1;i<=q;i++)
	{
		ui x=(get(s)^last)%n+1,k=(get(s)^last)%dep[x];
		last=ask(x,k);
//		cout<<x<<" "<<k<<" "<<last<<"\n";
		ans^=1ll*i*last;
	}
	printf("%lld",ans);
	return 0;
}

实测倍增 8.5s,长剖 4.65s,重剖 3.97s

长剖优化 dp

我们一步步引出长剖优化 dp 的思想是怎么出来的。
给出例题:CF1009F

很明显有 \(O(n^2)\) DP:\(f_{u,dep}=\sum\limits_{v\in u}f_{v,dep-1}\)
我们想想假如树是一条链的时候要怎么搞。
很明显,我们每次只有一个转移,只需要继承儿子的状态,然后所有数组后移一位不就可以了,做到 \(O(n)\)

那我们有两个儿子的情况呢?我们可以选择继承其中一个儿子的状态,然后暴力合并另一个的状态。单次时间复杂度是折半的。

那我们有很多个儿子的情况呢?我们如果是继承儿子状态,继承哪一个呢?很明显是深度最深的那个,也就是重儿子。然后我们暴力合并轻儿子。

有点类似 DSU on tree 的思想。
但是,与 DSU 不同的是,我们需要对某些进行清空,使得复杂度上升,但是,长剖可以直接继承儿子状态,这有什么优秀的性质呢?

  • 合并时,每条重链至多被扫一次。

这建立在 \(dep\) 维度的情况下。
这样,我们在重链就直接继承状态,有轻链才扫描。
所以长剖的时间复杂度是非常优秀的 \(O(n)\)

Q:为什么一定要选重儿子?
A:继承重儿子的状态可以存储所有深度状态,如果先存储轻儿子,时间复杂度理论正确,但是对于空间处理十分麻烦。而且,再次扫描重儿子时,重儿子可能会被扫描多次导致时间复杂度假了。

示例:

#include<bits/stdc++.h>
#define ll long long
#define N 1000005
#define ui unsigned int
using namespace std;
int n;
int fa[N];
int head[N],tot=1;
struct edge{
	int to,next;
}e[N*2];
void add(int u,int v)
{
	e[tot]=(edge){v,head[u]};
	head[u]=tot++;
}
int h[N],*f[N],Son[N];
void dfs1(int now,int fa)
{
	for(int i=head[now];i;i=e[i].next)
	{
		int son=e[i].to;
		if(son==fa) continue;
		dfs1(son,now);
		if(h[son]>h[now]) h[now]=h[son],Son[now]=son; 
	}
	h[now]++;
}
int g[N],siz;
int res[N];
void dfs2(int now,int fa)
{
	f[now][0]=1;
	if(Son[now])
	{
		f[Son[now]]=f[now]+1;
		dfs2(Son[now],now);
		res[now]=res[Son[now]]+1;
		if(f[now][res[now]]==1) res[now]=0;
	}
	else return;
	for(int i=head[now];i;i=e[i].next)
	{
		int son=e[i].to;
		if(son==fa||son==Son[now]) continue;
		f[son]=g+siz;siz+=h[son];
		dfs2(son,now);
		for(int j=1;j<=h[son];j++)
		{
			f[now][j]+=f[son][j-1];
			if(f[now][j]>f[now][res[now]]||f[now][j]==f[now][res[now]]&&j<res[now])
				res[now]=j;
		}
	}
	
}
int main()
{
	scanf("%d",&n);
	for(int i=1;i<n;i++)
	{
		int u,v;
		scanf("%d%d",&u,&v);
		add(u,v),add(v,u);
	}
	dfs1(1,0);
	f[1]=g,siz=h[1];
	dfs2(1,0);
	for(int i=1;i<=n;i++) printf("%d\n",res[i]);
	return 0;
}

总结一下,状态和深度有关可以使用长剖优化。
但是这些和长剖有关的树形 dp 十分恶心,我直接弃疗。

posted @ 2024-05-09 15:23  g1ove  阅读(9)  评论(0编辑  收藏  举报