什么是长链剖分?
轻重链剖分就是选择子树最大的儿子与当前点在同一条重链里,而长链剖分就是选择向下能达到的深度最深的儿子(也就是到叶子的链长度最长的儿子)与其在同一条长链中。
从任何一个点往上跳到根,最多经过 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 2r≥k,那么 y y y的第 k − r k-r k−r次祖先一定已经被记录在了它所在长链的链顶。
合理设 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 d−i。
那么转移就是对于 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,i−1)
f ( x , i ) + = f ( y , i − 1 ) f(x,i)+=f(y,i-1) f(x,i)+=f(y,i−1)
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,i−1)g(x,i)+g(y,i)f(x,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,i−1),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;
}