长链剖分学习笔记

什么是长链剖分?

轻重链剖分就是选择子树最大的儿子与当前点在同一条重链里,而长链剖分就是选择向下能达到的深度最深的儿子(也就是到叶子的链长度最长的儿子)与其在同一条长链中。

从任何一个点往上跳到根,最多经过 n \sqrt{n} n 条不同的长链。

灵魂画手重出江湖

根据长链的性质,这条长链链顶的父亲所在的长链,一定不会比它短,然后如图,最坏情况每条长链长度 n \sqrt{n} n ,一共 n \sqrt{n} n 条。

应用

1.求k次祖先

问题:给定一棵树,每次询问一共点的 k k k次祖先。

我会倍增! O ( n log ⁡ n ) + O ( Q log ⁡ n ) O(n \log n)+O(Q \log n) O(nlogn)+O(Qlogn)

而长链剖分可以做到 O ( n log ⁡ n ) + O ( Q ) O(n \log n)+O(Q) O(nlogn)+O(Q)

首先,因为长链剖分中每个点是在它往下延伸的最长链中,所以一个点的 k k k次祖先所在的长链长度一定大于等于 k k k

在每条长链的链顶,记录下整条链,和它的链长那么多次的祖先。这个记录是 O ( n ) O(n) O(n)的。

假如我们先跳到 x x x的第 r r r次祖先 y y y,并保证 2 r ≥ k 2r \geq k 2rk,那么 y y y的第 k − r k-r kr次祖先一定已经被记录在了它所在长链的链顶。

合理设 r r r的取值的话, r r r就设为 k k k在二进制下的最高位。于是只需要预处理每个 x x x的第 2 i 2^i 2i次祖先,就可以做到 O ( n log ⁡ n ) O(n \log n) O(nlogn)预处理, O ( 1 ) O(1) O(1)询问了。

题目链接

#include<bits/stdc++.h>
using namespace std;
#define RI register int
int read() {
	int q=0;char ch=' ';
	while(ch<'0'||ch>'9') ch=getchar();
	while(ch>='0'&&ch<='9') q=q*10+ch-'0',ch=getchar();
	return q;
}
const int N=300005;
int n,m,tot,ans,hbit[N],bin[23];
int h[N],ne[N<<1],to[N<<1],f[N][20],dep[N],d[N],top[N],son[N],len[N];
void add(int x,int y) {to[++tot]=y,ne[tot]=h[x],h[x]=tot;}
void dfs1(int x,int las) {
	d[x]=dep[x]=dep[las]+1,f[x][0]=las;
	for(RI i=1;i<=19;++i) f[x][i]=f[f[x][i-1]][i-1];
	for(RI i=h[x];i;i=ne[i]) {
		if(to[i]==las) continue;
		dfs1(to[i],x);
		if(d[to[i]]>d[x]) d[x]=d[to[i]],son[x]=to[i];
	}
}
void dfs2(int x,int las) {
	len[x]=d[x]-dep[top[x]]+1;
	if(!son[x]) return;
	top[son[x]]=top[x],dfs2(son[x],x);
	for(RI i=h[x];i;i=ne[i])
		if(to[i]!=las&&to[i]!=son[x]) top[to[i]]=to[i],dfs2(to[i],x);
}
vector<int> U[N],D[N];
int query(int x,int k) {
	if(k>=dep[x]) return 0;
	if(!k) return x;
	x=f[x][hbit[k]],k^=bin[hbit[k]];
	if(!k) return x;
	if(k==dep[x]-dep[top[x]]) return top[x];
	if(k>dep[x]-dep[top[x]]) return U[top[x]][k-dep[x]+dep[top[x]]-1];
	return D[top[x]][dep[x]-dep[top[x]]-k-1];
}
int main()
{
	int x,y,l;
	n=read();
	for(RI i=1;i<n;++i) x=read(),y=read(),add(x,y),add(y,x);
	dfs1(1,0),top[1]=1,dfs2(1,0);
	for(RI i=1;i<=n;++i) {
		if(i!=top[i]) continue;
		l=0,x=i;
		while(l<len[i]&&x) x=f[x][0],U[i].push_back(x),++l;
		l=0,x=i;
		while(l<len[i]) x=son[x],D[i].push_back(x),++l;
	}
	bin[0]=1;for(RI i=1;i<=20;++i) bin[i]=bin[i-1]<<1;
	for(RI i=1;i<=n;++i)
		for(RI j=20;j>=0;--j) if(bin[j]&i) {hbit[i]=j;break;}
	m=read();
	while(m--) {
		x=read()^ans,y=read()^ans;
		ans=query(x,y),printf("%d\n",ans);
	}
    return 0;
}

2.合并信息

例1 bzoj4543 Hotel加强版

首先写出DP形式的式子,设 f ( x , i ) f(x,i) f(x,i)表示 x x x的子树中距离 x x x i i i的点数, g ( x , i ) g(x,i) g(x,i)表示 x x x的子树里有多少个点对 ( a , b ) (a,b) (a,b),满足若 a , b a,b a,b到它们的lca点 o o o的距离为 d d d,则 o o o x x x的距离为 d − i d-i di

那么转移就是对于 x x x的每个儿子 y y y

g ( x , i ) + = g ( y , i + 1 ) + f ( x , i ) f ( y , i − 1 ) g(x,i)+=g(y,i+1)+f(x,i)f(y,i-1) g(x,i)+=g(y,i+1)+f(x,i)f(y,i1)

f ( x , i ) + = f ( y , i − 1 ) f(x,i)+=f(y,i-1) f(x,i)+=f(y,i1)

a n s + = f ( y , i − 1 ) g ( x , i ) + g ( y , i ) f ( x , i − 1 ) ans+=f(y,i-1)g(x,i)+g(y,i)f(x,i-1) ans+=f(y,i1)g(x,i)+g(y,i)f(x,i1)

发现对于第一个儿子,直接有 f ( x , i ) = f ( y , i − 1 ) , g ( x , i ) = g ( y , i + 1 ) f(x,i)=f(y,i-1),g(x,i)=g(y,i+1) f(x,i)=f(y,i1),g(x,i)=g(y,i+1)(边界 f ( x , 0 ) = 1 f(x,0)=1 f(x,0)=1)。

而假设 x x x到它所在长链的链底的距离为 l e n ( x ) len(x) len(x),那么 i i i的范围就是 [ 0 , l e n ( x ) ] [0,len(x)] [0,len(x)]

如果我在一个序列上合理的位置连续存储 f ( x , i ) f(x,i) f(x,i)的值,然后对于 x x x的长儿子(这么叫合适吗,是不是要叫重儿子?=。=) y y y,假设它存储的那一段连续的 f f f g g g的开始位置分别为 f ( y ) , g ( y ) f(y),g(y) f(y),g(y),那么 x x x的存储开始位置就应该是 f ( y ) − 1 f(y)-1 f(y)1 g ( y ) + 1 g(y)+1 g(y)+1,这样定位复杂度是 O ( 1 ) O(1) O(1)的。

对于轻儿子(说短儿子有点奇怪=。=)信息,则暴力往上合并。

每到一条长链链顶的父亲时,就要花费长链长度的复杂度合并信息,每条长链只有一个父亲,所以总复杂度是 O ( n ) O(n) O(n)的。

可以利用指针完成存储位置的定位操作。

#include<bits/stdc++.h>
using namespace std;
#define RI register int
int read() {
	int q=0;char ch=' ';
	while(ch<'0'||ch>'9') ch=getchar();
	while(ch>='0'&&ch<='9') q=q*10+ch-'0',ch=getchar();
	return q;
}
typedef long long LL;
const int N=100005;
int n,tot;LL ans,*g[N],*f[N],tmp[N<<2],*id=tmp;
int h[N],ne[N<<1],to[N<<1],len[N],son[N];

void add(int x,int y) {to[++tot]=y,ne[tot]=h[x],h[x]=tot;}
void dfs1(int x,int las) {
	len[x]=1;
	for(RI i=h[x];i;i=ne[i])
		if(to[i]!=las) {
			dfs1(to[i],x);
			if(len[to[i]]>len[son[x]]) len[x]=len[to[i]]+1,son[x]=to[i];
		}
}
void dfs2(int x,int las) {
	if(son[x]) f[son[x]]=f[x]+1,g[son[x]]=g[x]-1,dfs2(son[x],x);
	f[x][0]=1,ans+=g[x][0];
	for(RI i=h[x];i;i=ne[i]) {
		if(to[i]==las||to[i]==son[x]) continue;
		int y=to[i];f[y]=id,id+=len[y]<<1,g[y]=id,id+=len[y]<<1;
		dfs2(y,x);
		for(RI j=0;j<len[y];++j) {
			if(j) ans+=g[y][j]*f[x][j-1];
			if(j+1<len[x]) ans+=f[y][j]*g[x][j+1];
		}
		for(RI j=0;j<len[y];++j) {
			if(j+1<len[x]) g[x][j+1]+=f[y][j]*f[x][j+1];
			if(j) g[x][j-1]+=g[y][j];
			if(j+1<len[x]) f[x][j+1]+=f[y][j];
		}
	}
}
int main()
{
	int x,y;
	n=read();
	for(RI i=1;i<n;++i) x=read(),y=read(),add(x,y),add(y,x);
	dfs1(1,0),f[1]=id,id+=len[1]<<1,g[1]=id,id+=len[1]<<1,dfs2(1,0);
	printf("%lld\n",ans);
	return 0;
}

例2 codeforces 1009F Dominant Indices

可以不用指针,用精妙的编号方法来定位。

这种编号方法在写要用线段树维护DP的长链剖分的时候会比较方便……但我不记得那道题题号了……

#include<bits/stdc++.h>
using namespace std;
#define RI register int
int read() {
	int q=0;char ch=' ';
	while(ch<'0'||ch>'9') ch=getchar();
	while(ch>='0'&&ch<='9') q=q*10+ch-'0',ch=getchar();
	return q;
}
const int N=1000005;
int n,tot,tim;
int h[N],ne[N<<1],to[N<<1],pos[N],son[N],len[N],ans[N],f[N];

void add(int x,int y) {to[++tot]=y,ne[tot]=h[x],h[x]=tot;}
void dfs1(int x,int las) {
	len[x]=1;
	for(RI i=h[x];i;i=ne[i])
		if(to[i]!=las) {
			dfs1(to[i],x);
			if(len[to[i]]>len[son[x]]) son[x]=to[i],len[x]=len[to[i]]+1;
		}
}
void dfs2(int x,int las) {
	pos[x]=++tim,f[pos[x]]=1;
	if(son[x]) dfs2(son[x],x),ans[x]=ans[son[x]]+1;
	for(RI i=h[x];i;i=ne[i]) {
		if(to[i]==las||to[i]==son[x]) continue;
		int y=to[i];dfs2(y,x);
		for(RI j=0;j<len[y];++j) {
			f[pos[x]+j+1]+=f[pos[y]+j];
			if(f[pos[x]+j+1]>f[pos[x]+ans[x]]||
				(f[pos[x]+j+1]==f[pos[x]+ans[x]]&&j+1<ans[x])) ans[x]=j+1;
		}
	}
	if(f[pos[x]+ans[x]]==1) ans[x]=0;
}
int main()
{
	int x,y;
	n=read();
	for(RI i=1;i<n;++i) x=read(),y=read(),add(x,y),add(y,x);
	dfs1(1,0),dfs2(1,0);
	for(RI i=1;i<=n;++i) printf("%d\n",ans[i]);
	return 0;
}
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值