树的基本知识
树的存储
就按图的方式存储,因为树也是一种图啊~
无根树
如果你脑海里对于无根树的印象,只有“长得像树的东西”这一条,你就完蛋了…… \qquad ——于纪平
有根树
有一个节点为根,除根以外,每个节点有一个父节点。所有节点都能通过父亲的关系走到根,父节点不会形成环。
树的算法
因为树就是图,所以图的算法在树上都可以用。
无根树
⟺
\Longleftrightarrow
⟺ 无向图
有根树
⟺
\Longleftrightarrow
⟺ 有向图 (父亲指向孩子或者孩子指向父亲)
树的遍历
树的 D F S DFS DFS 序
从根开始
D
F
S
DFS
DFS ,根据访问的顺序可以得到一个
D
F
S
DFS
DFS 序列。
优点:一个子树在序列中对应着一个区间。可以将子树相关的题目转为序列上的区间问题,进而用线段树等方法解决。
void dfs2(int u, int tp) {
top[u] = tp; tid[u] = ++tim; rks[tim] = u;
if(son[u] == -1) return;
dfs2(son[u], tp);
for(int i = head[u]; ~i; i = edg[i].nxt) {
int v = edg[i].to;
if(v == son[u] || v == father[u]) continue;
dfs2(v, v);
}
}
无根树转有根树
选取一个点作为根:
- 一般情况下可以任取一个点作为,例如 1 1 1 号点。
- 但有些题目可能选取某些特殊的点更加方便
从根开始 D F S \ B F S DFS \backslash BFS DFS\BFS ,即可得出其他所有点的父亲。
有根树转无根树
把它变成一个图。 \qquad ——孙万华
树上的路径长度
假设边上带权。
- 求某点到其他点的距离: D F S \ B F S DFS \backslash BFS DFS\BFS 均正确。 O ( n ) O(n) O(n)
- 求所有点对间的距离:对于每一个点做一遍上面的操作。 O ( n 2 ) O(n^2) O(n2)
- 求两点间的距离:下面会提到 Q A Q QAQ QAQ 。
树的直径
定义:最远的两个点的路径。
求法:任取一个点
A
A
A ,
O
(
n
)
O(n)
O(n) 求出距离A最远的某个点
B
B
B ,
O
(
n
)
O(n)
O(n) 求出距离
B
B
B 最远的某个点
C
C
C 。则
B
B
B 与
C
C
C 之间的路径是一条直径。
直径可能不唯一,但是直径的长度一定唯一。
但是,当树上的边权为负值时,这个方法就是错误的。这时,我们就需要通过树形 d p dp dp 来求树的直径。
上图就是当边权为负值时的一个典型例子。我们通过树的直径的定义可知:直径是一棵树上边权值最大的那条路径。所以这棵树的直径应该是
6
→
7
6 \rightarrow 7
6→7 。而当我们按照两次
B
F
S
BFS
BFS 跑出来以后,我们发现第一次先跑到
4
4
4 号点,第二次时,无论是跑到
6
6
6 号点还是
7
7
7 号点,这条路径都不是这棵树的直径。所以这个算法是有局限性的。
d
p
dp
dp 做法:
我们不妨设
1
1
1 号节点为根,“
N
N
N 个点
N
−
1
N - 1
N−1 条边的无向图”就可以看作“有根树”。
设
D
[
x
]
D[x]
D[x] 表示从节点
x
x
x 出发走向以
x
x
x 为根的子树,能够到达最远节点的距离。设
x
x
x 的子节点为
y
1
,
y
2
,
y
3
.
.
.
.
.
.
y
t
y_1, y_2, y_3 ...... y_t
y1,y2,y3......yt ,
e
d
g
e
(
x
,
y
t
)
edge(x, y_t)
edge(x,yt) 表示边权,显然有:
D
[
x
]
=
m
a
x
1
≤
i
≤
t
{
D
[
y
i
]
+
e
d
g
e
(
x
,
y
i
)
}
D[x] = max_{1 ≤ i ≤ t} \left\{ D[y_i] + edge(x, y_i) \right\}
D[x]=max1≤i≤t{D[yi]+edge(x,yi)} 接下来,我们就可以考虑对于每个节点
x
x
x 求出“经过节点
x
x
x 的最长链的长度”——
F
[
x
]
F[x]
F[x],那整棵树的直径就是
m
a
x
1
≤
i
≤
t
F
[
x
]
max_{1 ≤ i ≤ t}{F[x]}
max1≤i≤tF[x] 。
那我们如何来求出
F
[
x
]
F[x]
F[x] 呢?
对于
x
x
x 的任意连个节点
y
i
y_i
yi 和
y
j
y_j
yj ,“经过节点
x
x
x 的最长链的长度” 可以通过四个部分构成:从
y
i
y_i
yi 到
y
i
y_i
yi 子树中最远的距离,边
e
d
g
e
(
y
i
,
x
)
edge(y_i, x)
edge(yi,x) ,边
e
d
g
e
(
x
,
y
j
)
edge(x, y_j)
edge(x,yj) ,和从
y
j
y_j
yj 到
y
j
y_j
yj 子树中最远的距离。不妨设
j
<
i
j < i
j<i ,因此:
F
[
x
]
=
m
a
x
1
≤
j
≤
i
≤
t
{
D
[
y
i
]
+
D
[
y
j
]
+
e
d
g
e
(
y
i
,
x
)
+
e
d
g
e
(
x
,
y
j
)
}
F[x] = max_{1 ≤ j ≤ i ≤ t} \left\{ D[y_i] + D[y_j] + edge(y_i, x) + edge(x, y_j) \right\}
F[x]=max1≤j≤i≤t{D[yi]+D[yj]+edge(yi,x)+edge(x,yj)} 但是我们没有必要使用两层循环来枚举
i
,
j
i, j
i,j 。在子节点的循环将要枚举到
i
i
i 时,
D
[
x
]
D[x]
D[x] 恰好就保存了从节点
x
x
x 出发走向“以
y
i
(
j
<
i
)
y_i (j < i)
yi(j<i) 为根的子树”,能够到达的最远节点的距离,这个距离就是
m
a
x
1
≤
j
<
i
{
D
[
y
j
]
+
e
d
g
e
(
x
,
y
j
)
}
max_{1 ≤ j < i} \left\{ D[y_j] + edge(x, y_j) \right\}
max1≤j<i{D[yj]+edge(x,yj)} 。所以,此时我们先用
D
[
x
]
+
D
[
y
i
]
+
e
d
g
e
(
x
,
y
i
)
D[x] + D[y_i] + edge(x, y_i)
D[x]+D[yi]+edge(x,yi) 更新
F
[
x
]
F[x]
F[x] ,再用
D
[
y
i
]
+
e
d
g
e
(
x
,
y
i
)
D[y_i] + edge(x, y_i)
D[yi]+edge(x,yi) 更新
D
[
x
]
D[x]
D[x] 。
struct E {
int nxt, to, val;
}e[maxn<<1];
int head[maxn];
void add(int u, int v, int w) {
static int cnt = 0;
e[++cnt] = (E) {head[u], v, w};
head[u] = cnt;
}
void dp(int x) {
v[x] = 1;
for(int i = head[x]; ~i; i = e[i].nxt) {
int v = e[i].to;
if(v[v]) continue;
dp(v);
ans = max(ans, d[x] + d[y] + e[i].val);
d[x] = max(d[x], d[y] + e[i].val);
}
}
有兴趣的同学可以做一下APIO2010 巡逻
树的中心
树的中点就是直径的中点。可能在节点上,也可能在边上。
求出直径就能求出中心了,但是要注意边权不为
1
1
1 的情况
中心是唯一的
求树的重心
查询 K K K 级祖先
每次给出点 x x x 和 k k k ,问 x x x 向根走 k k k 条边是哪个点。
暴力
复杂度: O ( h ) O(h) O(h)
倍增
复杂度: O ( n l o g n ) O(nlogn) O(nlogn)
O ( 1 ) O(1) O(1)询问做法
类似分块的思想。
令
S
=
n
S = \sqrt{n}
S=n ,预处理每个点的
1
,
2
,
3......
S
−
1
,
S
,
2
S
,
3
S
.
.
.
.
.
.
1, 2, 3 ...... S - 1, S, 2S, 3S ......
1,2,3......S−1,S,2S,3S...... 级的祖先。(这一步的时间复杂度
O
(
n
1.5
)
O(n^{1.5})
O(n1.5))
对于每次询问,通过两次查表得到答案。
树上动态规划
如果是无根树,先转为有根树。
需要按照自底向上或者自顶向下的顺序来处理
一般情况下在
D
F
S
\
B
F
S
DFS \backslash BFS
DFS\BFS的时候顺便处理就可以了
树上背包 d p dp dp
emmmm…把背包放在树上做
多叉树变二叉树
建模的方法
- 树的孩子兄弟表示法
学习树形结构时一定接触了一个多叉树变二叉树的方法,就是把每个点与它的第一个儿子连边,然后将它的儿子依次连接起来。可以结合下面的图片理解这句话。
S o : So: So: 第一个儿子是左子树,其他儿子是左子树的右子树。 -
D
F
S
DFS
DFS 序表示法
D F S DFS DFS 序就是对一个树进行一个 D F S DFS DFS ,然后对于每个点记录一个时间戳 D F N DFN DFN 数组,这个时间戳就是这个节点的 D F S DFS DFS 序,然后我们再记录一个 s i z e size size 数组,表示以每个节点为根的子树中节点的数量。
假设根节点是 u u u ,那么可以容易的推出:第一个儿子的 D F S DFS DFS 序 d f n [ f i r s t s o n ] dfn[first_{son}] dfn[firstson] 就是 d f n [ u ] + 1 dfn[u] + 1 dfn[u]+1 ,第二个儿子的 D F S DFS DFS 序 d f n [ s e c o n d s o n ] dfn[second_{son}] dfn[secondson] 就是 d f n [ u ] + s i z e [ f i r s t s o n ] + 1 dfn[u] + size[first_{son}] + 1 dfn[u]+size[firstson]+1 。那么我们可以推出 u u u 的下一个兄弟的 D F S DFS DFS 序 d f n [ n e x t b r o t h e r ] dfn[next_{brother}] dfn[nextbrother] 就是 d f n [ u ] + s i z e [ u ] + 1 dfn[u]+size[u]+1 dfn[u]+size[u]+1 。
这两种方法大多用于树形 d p dp dp 。这两个方法可以使决策转移的过程变为 O ( 1 ) O(1) O(1) 。
常见模型
有
n
n
n 个物品,有一个
m
m
m 大小的包,每个物品有
w
i
w_i
wi 物重与
v
i
v_i
vi 物品价值,物品之间存在只有装了物品
a
a
a ,才能装物品
b
b
b 的
n
−
1
n - 1
n−1 条关系(就是一个树)。问能取得的最大价值。
简要分析:显然是一个多叉树,那我们就要考虑转换。
- 树的孩子兄弟表示法
对于一个节点 i i i ,设 d p [ i ] [ j ] dp[i][j] dp[i][j] 表示在以 i i i 为根的树中,用大小为 j j j 的包能取得的最大价值,那么 d p [ i ] [ j ] = m a x ( d p [ l e f t [ i ] ] [ j − w [ i ] ] + v [ i ] , d p [ r i g h t [ i ] ] [ j ] ) dp[i][j] = max(dp[left[i]][j-w[i]] + v[i], dp[right[i]][j]) dp[i][j]=max(dp[left[i]][j−w[i]]+v[i],dp[right[i]][j]) 。
注意,这里的left[i]是i在原树中的第一个儿子,right[i]是i在原树中的下一个兄弟。
复杂度:O(nm) -
D
F
S
DFS
DFS 序表示法
对于 D F S DFS DFS 序为 i i i 的节点 u u u ,同样设 d p [ i ] [ j ] dp[i][j] dp[i][j] 表示在用大小为 j j j 的包能取得的最大价值,那么 d p [ i ] [ j ] + v [ i ] → d p [ i + 1 ] [ j − w [ i ] ] dp[i][j] + v[i] \rightarrow dp[i+1][j-w[i]] dp[i][j]+v[i]→dp[i+1][j−w[i]]
分组的树形背包
分析
下面针对这道题来分析,能够解决多叉树的,分组的树形背包。
此时,我们发现儿子与父亲之间并不存在依赖性关系。所以我们设
d
p
[
i
]
]
[
j
]
dp[i]][j]
dp[i]][j] 表示第
i
i
i 个节点切掉
j
j
j 个节点所需要的最少的边数。那么我们就可以得到递推式
d
p
[
u
]
[
j
]
=
min
(
d
p
[
u
]
[
j
]
,
d
p
[
u
]
[
j
−
k
]
+
d
p
[
v
]
[
k
]
)
(
u
当
前
节
点
,
v
子
节
点
)
dp[u][j] = \min (dp[u][j], dp[u][j-k] + dp[v][k]) (u当前节点, v子节点)
dp[u][j]=min(dp[u][j],dp[u][j−k]+dp[v][k])(u当前节点,v子节点) 同时还要注意
d
p
dp
dp 方程的边界问题:
d
p
[
u
]
[
s
i
z
[
u
]
]
=
1
dp[u][siz[u]]=1
dp[u][siz[u]]=1 将整个子树与切掉只需切掉与父节点的连边;
d
p
[
u
]
[
0
]
=
0
dp[u][0]=0
dp[u][0]=0 不切掉任何边;
d
p
[
1
]
[
s
i
z
[
1
]
]
=
0
dp[1][siz[1]] = 0
dp[1][siz[1]]=0 根节点无法与父亲节点切开。
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <algorithm>
using namespace std;
const int maxn = 237;
const int INF = 0x3f3f3f;
int read() {
int x = 0, f = 1; char c = getchar();
while(c < '0' || c > '9') {if(c == '-') f = -1; c = getchar();}
while(c >= '0' && c <= '9') {x = x * 10 + c - '0'; c = getchar();}
return f * x;
}
struct E {
int nxt, to;
}e[maxn<<1];
int head[maxn];
void add(int u, int v) {
static int cnt = 0;
e[++cnt] = (E) {head[u], v};
head[u] = cnt;
}
int n, p, k;
int siz[maxn], dp[maxn][maxn];
void dfs1(int u, int fa) {
siz[u]++;
for(int i = head[u]; ~i; i = e[i].nxt) {
int v = e[i].to;
if(v == fa) continue;
dfs1(v, u);
siz[u] += siz[v];
}
dp[u][siz[u]] = 1;
dp[u][0] = 0;
}
int ans = 0x3f3f3f;
void dfs2(int u, int fa) {
for(int i = head[u]; ~i; i = e[i].nxt) {
int v = e[i].to;
if(v == fa) continue;
dfs2(v, u);
for(int j = siz[u] - 1; j; j--) {
for(int k = 0; k <= j; k++) {
dp[u][j] = min(dp[u][j], dp[u][j-k] + dp[v][k]);
}
}
}
if(siz[u] - p >= 0) {
ans = min(ans, dp[u][siz[u]-p] + dp[u][siz[u]]);
}
}
int main() {
memset(head, -1, sizeof(head));
memset(dp, 0x3f3f3f, sizeof(dp));
n = read(), p = read();
for(int i = 1; i < n; i++) {
int u = read(), v = read();
add(u, v); add(v, u);
}
dfs1(1, 1);
dp[1][siz[1]] = 0;
dfs2(1, 1);
printf("%d", ans);
return 0;
}
/*
11 6
1 2
1 3
1 4
1 5
2 6
2 7
2 8
4 9
4 10
4 11
*/
有根树求两点间的 L C A LCA LCA
暴力
每次询问是 O ( h ) O(h) O(h) 。
向上标记法求 L C A LCA LCA
复杂度:
O
(
n
)
O(n)
O(n)(对于每次询问)
从
x
x
x 向上走到根节点,并标记所有经过的节点。
从
y
y
y 向上走到根节点,当第一次遇到已标记的节点时,就找到了
l
c
a
(
x
,
y
)
lca(x, y)
lca(x,y) 。
倍增求 L C A LCA LCA
复杂度:O(nlogn)
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <algorithm>
using namespace std;
const int maxn = 5e5 + 37;
int read() {
int x = 0, f = 1; char c = getchar();
while(c < '0' || c > '9') {if(c == '-') f = -1; c = getchar();}
while(c >= '0' && c <= '9') {x = x * 10 + c - '0'; c = getchar();}
return f * x;
}
struct E {
int nxt, to;
}e[maxn<<1];
int head[maxn];
void add(int u, int v) {
static int cnt = 0;
e[++cnt] = (E) {head[u], v};
head[u] = cnt;
}
int dep[maxn], fa[maxn][25], lg[maxn];
void dfs(int x, int father) {
dep[x] = dep[father] + 1;
fa[x][0] = father;
for(int i = 1; (1 << i) <= dep[x]; i++)
fa[x][i] = fa[fa[x][i-1]][i-1];
for(int i = head[x]; ~i; i = e[i].nxt)
if(e[i].to != father)
dfs(e[i].to, x);
}
int lca(int x, int y) {
if(dep[x] < dep[y]) swap(x, y);
while(dep[x] > dep[y]) x = fa[x][lg[dep[x]-dep[y]]-1];
if(x == y) return x;
for(int k = lg[dep[x]]; k >= 0; k--) {
if(fa[x][k] != fa[y][k]) {
x = fa[x][k], y = fa[y][k];
}
}
return fa[x][0];
}
int n, m, s;
void init() {
for(int i = 1; i <= n; i++)
lg[i] = lg[i-1] + (1 << lg[i-1] == i);
}
int main() {
memset(head, -1, sizeof(head));
n = read(), m = read(), s = read();
for(int i = 1; i < n; i++) {
int u = read(), v = read();
add(u, v); add(v, u);
}
dfs(s, 0);
init();
for(int i = 1; i <= m; i++) {
int u = read(), v = read();
printf("%d\n", lca(u, v));
}
return 0;
}
树链剖分求 L C A LCA LCA
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <algorithm>
using namespace std;
const int maxn = 5e5 + 37;
int read() {
int x = 0, f = 1; char c = getchar();
while(c < '0' || c > '9') {if(c == '-') f = -1; c = getchar();}
while(c >= '0' && c <= '9') {x = x * 10 + c - '0'; c = getchar();}
return f * x;
}
struct E {
int nxt, to;
}e[maxn<<1];
int head[maxn];
void add(int u, int v) {
static int cnt = 0;
e[++cnt] = (E) {head[u], v};
head[u] = cnt;
}
int fa[maxn], dep[maxn], siz[maxn], son[maxn], top[maxn];
void dfs1(int x) {
siz[x] = 1; dep[x] = dep[fa[x]] + 1;
for(int i = head[x]; ~i; i = e[i].nxt) {
int v = e[i].to;
if(v == fa[x]) continue;
fa[v] = x;
dfs1(v);
siz[x] += siz[v];
if(!son[x] || siz[son[x]] < siz[v]) son[x] = v;
}
}
void dfs2(int x, int tp) {
top[x] = tp;
if(son[x]) dfs2(son[x] , tp);
for(int i = head[x]; ~i; i = e[i].nxt) {
int v = e[i].to;
if(v == fa[x] || v == son[x]) continue;
dfs2(v, v);
}
}
int lca(int x, int y) {
while(top[x] != top[y]) {
if(dep[top[x]] > dep[top[y]]) x = fa[top[x]];
else y = fa[top[y]];
}
return dep[x] < dep[y] ? x : y;
}
int n, m, s;
int main() {
memset(head, -1, sizeof(head));
n = read(), m = read(), s = read();
for(int i = 1; i < n; i++) {
int u = read(), v = read();
add(u, v); add(v, u);
}
dfs1(s);
dfs2(s, s);
for(int i = 1; i <= m; i++) {
int x = read(), y = read();
printf("%d\n", lca(x, y));
}
return 0;
}
T a r j a n Tarjan Tarjan 求 L C A LCA LCA
树上两点间距离
暴力
复杂度: O ( n 2 ) O(n^2) O(n2)
用 L C A LCA LCA
复杂度:O(nlogn)
如果是无根树,先转有根树。预处理出
d
[
x
]
d[x]
d[x] 表示从根到位置
x
x
x 的边的权值和,则从
i
i
i 到
j
j
j 的距离为:
d
[
i
]
+
d
[
j
]
−
2
d
[
l
c
a
(
i
,
j
)
]
d[i] + d[j] - 2d[lca(i, j)]
d[i]+d[j]−2d[lca(i,j)] 。
Q:那如果求
x
x
x 到
y
y
y 的路径上,边权的最大值呢?
A:在
l
c
a
lca
lca 的倍增数组之外,额外预处理
f
(
i
,
j
)
f(i, j)
f(i,j) 表示从
i
i
i 向根走
2
j
2^j
2j 条边时,路径上的最大值。
Q:如果求的是路径上的点权值的和呢?
A:同样,现将无根树转为有根树。预处理出
d
[
x
]
d[x]
d[x] 表示从根到位置
x
x
x 的点的权值和,则从
i
i
i 到
j
j
j 的距离为 :
d
[
i
]
+
d
[
j
]
−
2
d
[
l
c
a
(
i
,
j
)
]
+
v
a
l
[
l
c
a
(
i
,
j
)
]
d[i] + d[j] - 2d[lca(i, j)] + val[lca(i, j)]
d[i]+d[j]−2d[lca(i,j)]+val[lca(i,j)] 。
有根树的链剖分
链剖分
定义一棵树的链剖分:从所有的边中选出一个子集,称为“实边”。如果这个集合满足:对于任何一个节点,它连向孩子的边至多有一条为实边(即在集合中);那么我们称这个集合为这棵树的一种链剖分。树的链剖分可能不唯一。不在集合中的边称为“虚边”。
Q:为什么叫链剖分?
A:形象地来看,假如删掉所有的虚边,仅保留实边,则树剩下了若干条不相交的链,且每个点恰好在一条链上。(链的长度可能为
1
1
1 )
树链剖分
一般来说,“轻重链剖分”、“重链剖分”都是指的这个。
剖分依据:
预处理
S
(
x
)
S(x)
S(x) ,表示点
x
x
x 的子树中节点个数。
对于每个点,选取
S
S
S 最大的孩子,将这条边选为实边。
可以
O
(
n
)
O(n)
O(n) 完成剖分
树链剖分的性质
任何一个点到跟的路径上,至多有
O
(
l
o
g
n
)
O(logn)
O(logn) 条轻边。这是因为,每经过一条轻边,子树大小(即
S
S
S 值)至少变为
2
2
2 倍。也就是说,任何一点到根的路径,可以分成
O
(
l
o
g
n
)
O(logn)
O(logn) 段,每一段都是某条重链的连续一段。
**推论:**推论:对于任意一个点
x
x
x ,和
x
x
x 的某个祖先
y
y
y ,从
x
x
x 到
y
y
y 的路径可以分成
O
(
l
o
g
n
)
O(logn)
O(logn) 段,每一段都是某条重链的连续一段。
**结论:**对于任意两个点
x
x
x 和
y
y
y ,从
x
x
x 到
y
y
y 的路径可以分成
O
(
l
o
g
n
)
O(logn)
O(logn) 段,每一段都是某条重链的连续一段。