文章目录
概述
与重链剖分类似,但是假如重链剖分找的是重儿子,那长链剖分找的就是深儿子。
可以在 O ( n ) O(n) O(n) 的时间复杂度解决只需要维护子树中与深度有关的信息的问题。
所以说是一种很牛的 trick ,但是好像没有重链剖分和 dsu on tree 常用。
性质
摘抄自 yyb 的博客
- 所有链的长度和是 O ( n ) O(n) O(n) 的
- 任意一个点的 k k k 次祖先所在的长链的长度大于等于 k k k
- 任意一个点向上跳跃到根所经过的链的条数是 O ( n ) O(\sqrt n) O(n) 的。
第 1 条性质决定了长链剖分在处理深度相关问题时的优越时间空间复杂度。
第 3 条性质说明他在处理树上两点之间的问题时没有重链剖分优秀。
做题的时候各取所需就好了。
例题
下面是几道例题。第一题会讲得详细一点,包含了如何用指针写法节省空间。
【POI2014】 hotel
题意
给一棵树,求满足 d i s ( a , b ) = d i s ( b , c ) = d i s ( c , a ) dis(a,b)=dis(b,c)=dis(c,a) dis(a,b)=dis(b,c)=dis(c,a) 的无序三元组 ( a , b , c ) (a,b,c) (a,b,c) 的个数。
思路
发现三元组有两种形态:
- 三个点有共有的 LCA
2. 没有共有的 LCA
为了方便去重,对于第 1 种情况,我们只在 1 号点统计答案;对于第 2 种情况,我们只在 2 号点统计答案(如图)。
记录两个 DP 值:
- f [ i ] [ j ] f[i][j] f[i][j] 表示 i i i 的子树内距离 i i i 为 j j j 的点个数
- g [ i ] [ j ] g[i][j] g[i][j] 表示 i i i 的子树内,已经有一个点对,还需要在 i i i 之外连一条长度为 j j j 的链才能形成三元组的点对个数
转移和贡献答案就非常方便了。
瞎扯
这题的 DP 状态就挺难设的,设不出方便转移的状态,长链剖分就很难发挥他的长处。
由此题可以发现,长链剖分很难搞子树之外的东西。因为子树之外的一般要换根 DP ,而长链剖分并不能记录所有节点的所有信息。
所以在设计状态的时候要尽量只利用子树信息,并且可以快速地继承和合并。
并且建议第一道题先找一篇好看的代码,对着抄一遍,然后就会打了。
代码
抄来的指针写法,实在是太高妙了。
// POI 2014 hotel
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 1e5 + 10, M = N<<1;
int n;
int h[N], ecnt, nxt[M], v[M];
int dep[N], son[N];
LL *f[N], *g[N], tmp[N<<2], *id = tmp, ans;
void add(int x, int y){
nxt[++ecnt] = h[x]; v[ecnt] = y;
h[x] = ecnt;
}
void dfs(int u, int fa){
for (int i = h[u]; i; i = nxt[i])
if (v[i] != fa){
dfs(v[i], u);
if (dep[v[i]] > dep[u])
son[u] = v[i], dep[u] = dep[v[i]];
}
dep[u]++;
}
void dfs1(int u, int fa){
if (son[u]){
f[son[u]] = f[u]+1;
g[son[u]] = g[u]-1;
dfs1(son[u], u);
}
f[u][0] = 1; ans += g[u][0];
for (int i = h[u]; i; i = nxt[i])
if (v[i] != fa && v[i] != son[u]){
f[v[i]] = id; id += dep[v[i]]<<1;
g[v[i]] = id; id += dep[v[i]]<<1;
dfs1(v[i], u);
for (int j = 0; j < dep[v[i]]; ++ j){
if (j) ans += g[v[i]][j] * f[u][j-1];
ans += g[u][j+1] * f[v[i]][j];
}
for (int j = 0; j < dep[v[i]]; ++ j){
g[u][j+1] += f[u][j+1] * f[v[i]][j];
f[u][j+1] += f[v[i]][j];
if (j) g[u][j-1] += g[v[i]][j];
}
}
}
int main()
{
scanf("%d", &n);
ecnt = 1;
for (int i = 1; i < n; ++ i){
int x, y;
scanf("%d%d", &x, &y);
add(x, y); add(y, x);
}
memset(dep, 0, sizeof dep);
memset(son, 0, sizeof son);
dfs(1, 0);
memset(tmp, 0, sizeof tmp);
f[1] = id; id += dep[1]<<1;
g[1] = id; id += dep[1]<<1;
dfs1(1, 0);
printf("%lld\n", ans);
return 0;
}
【vijos】lxhgww 的奇思妙想
题意
给一棵树,多次询问求 u u u 的 k k k 次祖先,强制在线。
n ≤ 3 ∗ 1 0 5 , q ≤ 1.8 ∗ 1 0 6 n\le 3*10^5,q\le 1.8*10^6 n≤3∗105,q≤1.8∗106
思路
思路清奇。是倍增做法的长链剖分优化。
做法是 O ( n log n ) O(n\log n) O(nlogn) 预处理, O ( 1 ) O(1) O(1) 查询 k k k 次祖先。
首先处理出倍增数组,然后对每个长链顶端,预处理他向上 d e p [ t o p ] dep[top] dep[top] 和向下 d e p [ t o p ] dep[top] dep[top] 各是哪些点, d e p [ t o p ] dep[top] dep[top] 就是他所在长链的长度。
对于每次询问,先向上跳 2 r 2^r 2r 步到 v v v, r r r 是 k k k 二进制下最高位的 1 ,然后再跳到 v v v 所在长链顶端 t o p top top 。因为 2 r ≥ k 2 2^r\ge \frac{k}{2} 2r≥2k ,而根据上面的性质 2 , t o p top top 必然存着向上向下至少 2 r 2^r 2r 个点的信息。所以只要暴力查询就行了。
代码
#include<bits/stdc++.h>
using namespace std;
const int N = 3e5 + 10, M = N<<1, E = 20;
namespace Graph
{
int h[N], ecnt, nxt[M], v[M];
void clear(){ecnt = 1;}
void add_dir(int _u, int _v){
v[++ecnt] = _v;
nxt[ecnt] = h[_u]; h[_u] = ecnt;
}
void add_undir(int _u, int _v){
add_dir(_u, _v);
add_dir(_v, _u);
}
}
using namespace Graph;
int n, q, f[N][E], highbit[N], dep[N], son[N], top[N], dis[N];
int tmp[N*3], *up[N], *dn[N], *id = tmp, ans;
template<class T>inline void read(T &x){
x = 0; bool fl = 0; char c = getchar();
while (!isdigit(c)){if (c == '-') fl = 1; c = getchar();}
while (isdigit(c)){x = (x<<3)+(x<<1)+c-'0'; c = getchar();}
if (fl) x = -x;
}
template<class T>inline void wr(T x){
if (x < 0) x = -x, putchar('-');
if (x > 9) wr(x / 10);
putchar(x % 10 + '0');
}
template<class T>inline void wrl(T x){
wr(x); puts("");
}
void dfs(int u, int fa){
f[u][0] = fa;
for (int i = 1; i < E; ++ i)
f[u][i] = f[f[u][i-1]][i-1];
for (int i = h[u]; i; i = nxt[i])
if (v[i] != fa){
dfs(v[i], u);
if (dep[u] < dep[v[i]])
dep[u] = dep[v[i]], son[u] = v[i];
}
dep[u]++;
}
void dfs1(int u, int fa, bool flag, int _tp){
if (flag){
for (int i = 0, w = u; i < dep[u]; ++ i, w = f[w][0])
up[u][i] = w;
}
if (flag) dis[u] = 0;
else dis[u] = dis[fa]+1;
top[u] = _tp;
dn[u][0] = u;
if (son[u]){
dn[son[u]] = dn[u]+1;
dfs1(son[u], u, 0, _tp);
}
for (int i = h[u]; i; i = nxt[i])
if (v[i] != fa && v[i] != son[u]){
up[v[i]] = id; id += dep[v[i]];
dn[v[i]] = id; id += dep[v[i]];
dfs1(v[i], u, 1, v[i]);
}
}
inline int query(int u, int k){
if (k == 0) return u;
u = f[u][highbit[k]];
k -= 1<<highbit[k];
if (!u) return 0;
k -= dis[u];
u = top[u];
if (k >= 0) return up[u][k];
else return dn[u][-k];
}
int main()
{
read(n);
clear();
for (int i = 1; i < n; ++ i){
int x, y;
read(x); read(y);
add_undir(x, y);
}
highbit[1] = 0;
for (int i = 2; i <= n; ++ i)
highbit[i] = highbit[i>>1]+1;
dfs(1, 0);
dn[1] = id; id += dep[1];
up[1] = id; id += dep[1];
dfs1(1, 0, 1, 1);
for (read(q), ans = 0; q--; ){
int x, y;
read(x); read(y);
x ^= ans; y ^= ans;
ans = query(x, y);
wrl(ans);
}
return 0;
}
【bzoj3653】 谈笑风生
题意
设 T 为一棵有根树,我们做如下的定义:
-
设 a 和 b 为 T 中的两个不同节点。如果 a 是 b 的祖先,那么称“a 比 b 不知道高明到哪里去了”。
-
设 a 和 b 为 T 中的两个不同节点。如果 a 与 b 在树上的距离不超过某个给定常数 x,那么称“a 与 b 谈笑风生”。
给定一棵 n 个节点的有根树 T,节点的编号为 1 ∼ n,根节点为 1 号节点。你需要回答 q 个询问,询问给定两个整数 p 和 k,问有多少个有序三元组 (a; b; c) 满足:
-
a、 b 和 c 为 T 中三个不同的点,且 a 为 p 号节点;
-
a 和 b 都比 c 不知道高明到哪里去了;
-
a 和 b 谈笑风生。这里谈笑风生中的常数为给定的 k。
瞎扯
从这题可以发现,长链剖分搞不来在线的询问 (这不显然,数组继承之后瞎搞,原来的版本早就不见了)。
所以这题还可以用 dfs 序主席树做成在线的。
但是假如毒瘤出题人一定要卡
O
(
n
log
n
)
O(n\log n)
O(nlogn) ,那么长链剖分的
O
(
n
)
O(n)
O(n) 复杂度就显得非常优越了 (虽然我的写法还是带了 log ,但的确是可以不带的)。
思路
所以我们考虑这个有序的三元组,他们在一条直上直下的链上,从浅到深依次为 ( a , b , c ) (a,b,c) (a,b,c) 或 ( b , a , c ) (b,a,c) (b,a,c) 。
对于 ( b , a , c ) (b,a,c) (b,a,c) , 答案就是 a a a 上面有多少距离小于等于 k k k 的祖先,乘上 a a a 的子孙个数。
对于 ( a , b , c ) (a,b,c) (a,b,c) ,需要统计子树中到 a a a 的距离小于等于 k k k 的所有点的子孙个数之和。用长链剖分维护一下就好了。
代码
// bzoj 3653 luogu 3899
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 3e5 + 10, M = N<<1;
namespace Graph
{
int h[N], ecnt, nxt[M], v[M];
void clear(){ecnt = 1;}
void add_dir(int _u, int _v){
v[++ecnt] = _v;
nxt[ecnt] = h[_u]; h[_u] = ecnt;
}
void add_undir(int _u, int _v){
add_dir(_u, _v);
add_dir(_v, _u);
}
}
using namespace Graph;
int n, m, dep[N], son[N], dpt[N], siz[N]; // dep[i] 子树深度; dpt[i] 到根距离
struct node{
int p, k, id;
}q[N];
node *qry[N];
LL ans[N], tmp[N], *id = tmp, *f[N];
template<class T>inline void read(T &x){
x = 0; bool fl = 0; char c = getchar();
while (!isdigit(c)){if (c == '-') fl = 1; c = getchar();}
while (isdigit(c)){x = (x<<3)+(x<<1)+c-'0'; c = getchar();}
if (fl) x = -x;
}
template<class T>inline void wr(T x){
if (x < 0) x = -x, putchar('-');
if (x > 9) wr(x / 10);
putchar(x % 10 + '0');
}
template<class T>inline void wrl(T x){
wr(x); puts("");
}
bool cmp(node x, node y){
if (x.p != y.p) return x.p < y.p;
return x.k < y.k;
}
void dfs1(int u, int fa){
dpt[u] = dpt[fa]+1;
siz[u] = 1;
for (int i = h[u]; i; i = nxt[i])
if (v[i] != fa){
dfs1(v[i], u);
siz[u] += siz[v[i]];
if (dep[v[i]] > dep[u])
dep[u] = dep[v[i]], son[u] = v[i];
}
dep[u]++;
}
void dfs(int u, int fa){
if (son[u]){
f[son[u]] = f[u]+1;
dfs(son[u], u);
}
for (int i = h[u]; i; i = nxt[i])
if (v[i] != fa && v[i] != son[u]){
f[v[i]] = id; id += dep[v[i]];
dfs(v[i], u);
for (int j = 0; j < dep[v[i]]; ++ j)
f[u][j+1] += f[v[i]][j];
}
f[u][0] = f[u][1] + siz[u]-1;
for (int i = 0; qry[u][i].p == u; ++ i){
ans[qry[u][i].id] += f[u][1]-(qry[u][i].k+1 < dep[u] ? f[u][qry[u][i].k+1] : 0);
}
}
int main()
{
read(n); read(m);
clear();
for (int i = 1; i < n; ++ i){
int x, y;
read(x); read(y);
add_undir(x, y);
}
for (int i = 1; i <= m; ++ i){
read(q[i].p); read(q[i].k);
q[i].id = i;
}
sort(q + 1, q + m + 1, cmp);
for (int i = 1; i <= n; ++ i) qry[i] = q + 1;
for (int i = m; i >= 1; -- i)
qry[q[i].p] = q + i;
dfs1(1, 0);
f[1] = id; id += dep[1];
dfs(1, 0);
for (int i = 1; i <= m; ++ i)
ans[q[i].id] += 1LL*min(q[i].k, dpt[q[i].p]-1)*(siz[q[i].p]-1);
for (int i = 1; i <= m; ++ i)
wrl(ans[i]);
return 0;
}
【香蕉OI】 缘分
题意
求满足下列条件的无序四元组 ( a , b , c , d ) (a,b,c,d) (a,b,c,d) 个数:
- 路径 ( a , c ) (a,c) (a,c) 和路径 ( b , d ) (b,d) (b,d) 有交,设交于路径 ( e , f ) (e,f) (e,f)
- a , b a,b a,b 在 e e e 的一侧, c , d c,d c,d 在 f f f 的一侧
- d i s ( a , e ) = d i s ( b , e ) = d i s ( c , f ) = d i s ( d , f ) dis(a,e)=dis(b,e)=dis(c,f)=dis(d,f) dis(a,e)=dis(b,e)=dis(c,f)=dis(d,f)
思路
做过上面的 hotel 那题之后,这题就完全是套路了。
记三个 DP 如下:
- f [ u ] [ j ] f[u][j] f[u][j] 表示 u u u 的子树中深度为 j j j 的点的个数
- g [ u ] [ j ] g[u][j] g[u][j] 表示 u u u 的子树中两个点距离 LCA 的距离都为 j j j 的点对的个数
- h [ u ] [ j ] h[u][j] h[u][j] 表示 u u u 的子树中 a , b , c a,b,c a,b,c 已经确定,还需要一个距离 u u u 为 j j j 的点就可以构成答案的三元组个数
可以把这几种情况先画出来,然后转移就非常显然了。
答案可以由两个 g g g 贡献,也可以由一个 h h h 和一个 f f f 贡献。
注意
特别注意:
更新 DP 数组的时候一定要注意顺序,因为某个数组的更新以来的可能是上一个版本的数组。比如本题有 f , g , h f,g,h f,g,h 三个数组,必须要依次更新 h , g , f h,g,f h,g,f ,依赖关系就比如 h h h 更新依赖的是上一个版本的 g , f g,f g,f 。
代码
#include<bits/stdc++.h>
using namespace std;
const int N = 4e5 + 10, M = N<<1, mod = 998244353;
int n, hh[N], ecnt, nxt[M], v[M];
int dep[N], son[N];
int tmp[N*6], *f[N], *g[N], *h[N], *id = tmp, ans;
int add(int &x, int y){x += y; if (x >= mod) x -= mod;}
void _add(int x, int y){
nxt[++ecnt] = hh[x]; v[ecnt] = y;
hh[x] = ecnt;
}
void dfs1(int u, int fa){
for (int i = hh[u]; i; i = nxt[i])
if (v[i] != fa){
dfs1(v[i], u);
if (dep[v[i]] > dep[son[u]])
dep[u] = dep[v[i]], son[u] = v[i];
}
dep[u]++;
}
void dfs(int u, int fa){
if (son[u]){
f[son[u]] = f[u]+1;
g[son[u]] = g[u];
h[son[u]] = h[u]-1;
dfs(son[u], u);
}
f[u][0] = 1; add(ans, h[u][0]);
for (int i = hh[u]; i; i = nxt[i])
if (v[i] != fa && v[i] != son[u]){
f[v[i]] = id; id += dep[v[i]];
g[v[i]] = id; id += dep[v[i]]<<1;
h[v[i]] = id; id += dep[v[i]];
dfs(v[i], u);
for (int j = 0; j < dep[v[i]]; ++ j){
add(ans, 1LL*g[u][j]*g[v[i]][j]%mod);
add(ans, 1LL*h[u][j+1]*f[v[i]][j]%mod);
if (j) add(ans, 1LL*f[u][j-1]*h[v[i]][j]%mod);
}
for (int j = 0; j < dep[v[i]]; ++ j){ // attention!!!
if (j) add(h[u][j-1], h[v[i]][j]);
add(h[u][j], 1LL*f[u][j]*g[v[i]][j]%mod);
add(h[u][j+1], 1LL*g[u][j+1]*f[v[i]][j]%mod);
}
for (int j = 0; j < dep[v[i]]; ++ j){
add(g[u][j], g[v[i]][j]);
add(g[u][j+1], 1LL*f[u][j+1]*f[v[i]][j]%mod);
}
for (int j = 0; j < dep[v[i]]; ++ j){
add(f[u][j+1], f[v[i]][j]);
}
}
}
int main()
{
scanf("%d", &n);
for (int i = 1; i < n; ++ i){
int x, y;
scanf("%d%d", &x, &y);
_add(x, y); _add(y, x);
}
dfs1(1, 0);
f[1] = id; id += dep[1];
g[1] = id; id += dep[1]<<1;
h[1] = id; id += dep[1];
dfs(1, 0);
printf("%d\n", ans);
return 0;
}
后记
长链剖分作为一种处理树上信息的技巧,需要熟练掌握,并灵活运用。
要是有好题还会继续更新。