[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 十分恶心,我直接弃疗。