fjzzq2002
租酥雨
oi-wiki
长链剖分是和重链剖分,dsu on tree一类的trick,不过它的适用范围与dsu on tree不同,它适用与涉及到深度的查询。
先来定义一下长链剖分是什么。它与重链剖分十分类似,但是原来的重儿子是子树大小最大的儿子,现在的"重"儿子(因为不是重儿子了,大概是preffered child)是往下延伸到叶子节点那条链最长的儿子。
整个长链剖分的过程与轻重链剖分的过程类似,只要一遍dfs即可。
具体实现方式&复杂度证明:
对每个点寻找深度最大的儿子作为重儿子,其余作为轻儿子。由此得到了若干条互不相交的长链。
在维护信息的过程中,先继承重儿子的信息,再暴力合并其余轻儿子的信息。
因为每个点仅属于一条长链,且一条长链只会在链顶位置作为轻儿子暴力合并一次,所以时间复杂度线性。
在继承重儿子信息这点上有不同的实现方式,一个巧妙的方法是利用指针实现,具体可以参见代码。
长链剖分优化 DP
一般情况下可以使用长链剖分来优化的 DP 会有一维状态为深度维。
我们可以考虑使用长链剖分优化树上 DP。
具体的,我们每个节点的状态直接继承其重儿子的节点状态,同时将轻儿子的 DP 状态暴力合并。
例题:codeforces1009F 这好像也是一道dsu on tree板子题(
题意:给定一棵以1为根,个节点的树。设为子树中到距离为的节点数。
对于每个点,求一个最小的,使得最大。
设表示在子树内,和距离的点数。
暴力转移时间复杂度为
我们考虑每次转移我们直接继承重儿子的DP数组和答案,并且考虑在此基础上进行更新。
首先我们需要将重儿子的DP数组前面插入一个元素1,这代表着当前节点。
然后我们将所有亲儿子的DP数组暴力和当前节点的DP数组合并。
注意到因为轻儿子的DP数组长度为轻儿子所在重链长度,而所有重链长度和为。
也就是说,我们直接暴力合并轻儿子的总时间复杂是。
注意,一般情况下DP数组的内存分配为一条重链整体分配内存,链上不同的节点有不同的首位置指针。
DP数组的长度我们可以根据子树最深节点算出。
代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
|
const int N=2111111;
vector<int>e[N];
int n,fa[N];
int d[N],pson[N];
int dfn[N],tim;
int *f[N],g[N];
int ans[N];
void dfs1(int u)
{
d[u]=1;
for(int v:e[u]) if(v^fa[u])
{
fa[v]=u;
dfs1(v);
ckmax(d[u],d[v]+1);
pson[u]=(d[v]>d[pson[u]])?v:pson[u];
}
}
void dfs2(int u)
{
dfn[u]=++tim;
f[u]=g+dfn[u];
if(pson[u]) dfs2(pson[u]);
for(int v:e[u]) if(v^fa[u]&&v^pson[u]) dfs2(v);
}
void dfs3(int u)
{
if(pson[u])
{
dfs3(pson[u]);
ans[u]=ans[pson[u]]+1;
}
f[u][0]=1;
if(f[u][ans[u]]<=1) ans[u]=0;
for(int v:e[u]) if(v^fa[u]&&v^pson[u])
{
dfs3(v);
R(j,0,d[v]-1)
{
f[u][j+1]+=f[v][j];
if(f[u][j+1]>f[u][ans[u]]) ans[u]=j+1;
if(f[u][j+1]==f[u][ans[u]]&&j+1<ans[u]) ans[u]=j+1;
}
}
}
signed main()
{
n=read();
int u,v;R(i,2,n)u=read(),v=read(),e[u].pb(v),e[v].pb(u);
dfs1(1);dfs2(1);dfs3(1);
R(i,1,n) writeln(ans[i]);
}
|
例题:lgP5904[POI2014]Hot-Hotels
题意:给你一棵树,从中选3个点,两两距离相等,求方案数。
考虑暴力dp。令表示**子树**内与距离为的点的个数。但是这样直接对每个节点深度进行统计会重复。所以再令表示的子树内,有多少二元组满足满足。
可以推出节点对答案的贡献为
考虑的转移为
接下来考虑的转移,可以有两种从儿子转移的方式:

对于第一种情况,就是少了一个。对于第二种情况,发现,那么,又因为到自身距离为0,于是有:
然后可以长剖将时间复杂度优化为
简易版:
设表示子树内与距离为的点的个数,表示子树内,满足第三个点和距离为的点对数目。
这样每次可以拿和更新答案,拿更新,更新,更新。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
|
const int N=444444;
vector<int>e[N];
int d[N],pson[N];
int n;
int fa[N];
int dfn[N],tim;
int tmp[N],*id=tmp;
int *f[N],*g[N],q[N];
int ans;
void dfs1(int u)
{
for(int v:e[u]) if(v^fa[u])
{
fa[v]=u;
dfs1(v);
pson[u]=(d[v]>d[pson[u]])?v:pson[u];
}
d[u]=d[pson[u]]+1;
}
void dfs3(int u)
{
if(pson[u]) f[pson[u]]=f[u]+1,g[pson[u]]=g[u]-1,dfs3(pson[u]);
f[u][0]=1;ans+=g[u][0];
for(int v:e[u]) if(v^fa[u]&&v^pson[u])
{
f[v]=id;id+=(d[v]<<1);g[v]=id;id+=(d[v]<<1);
dfs3(v);
R(j,0,d[v]-1)
{
if(j) ans+=f[u][j-1]*g[v][j];
ans+=g[u][j+1]*f[v][j];
}
R(j,0,d[v]-1)
{
g[u][j+1]+=f[u][j+1]*f[v][j];
if(j) g[u][j-1]+=g[v][j];
f[u][j+1]+=f[v][j];
}
}
}
signed main()
{
n=read();
int u,v;R(i,2,n) u=read(),v=read(),e[u].pb(v),e[v].pb(u);
dfs1(1);
f[1]=id;id+=(d[1]<<1);g[1]=id;id+=(d[1]<<1);
dfs3(1);
writeln(ans);
}
|
例题:k-th ancestor query
https://zhuanlan.zhihu.com/p/25984772
性质:任意一个点的k级祖先所在链的链长一定。这个证明十分简单,因为它到祖先长度就为k了,如果它到祖先不是重链,链长肯定不会更短。
求出长链剖分之后我们对于每条重链的链头,设这条重链长度为len,记录一下这条重链每个深度的点,然后记录链头往上len个点是哪些(显然不会影响复杂度,顶多*2)。
我们同样预处理倍增跳的数组,但是我们还要记录一下1~n每个数的high-bit(最高位的1在哪)。
我们现在考虑向上跳k,那么我们先跳k的最高位r,然后还要跳k-r这么高。考虑这个点往上r的祖先,那么它所在重链链长>=r,注意到k-r<r,那么我们只要在链头向上向下跳就可以了。