树的基本性质(8个经典模板套路)
树的基本性质——前置知识
1. 树的性质
树的基本性质:
- 树的边数:对于一个有 nnn 个节点的树,其边数为 n−1n - 1n−1 条。这个性质可以通过归纳法或者集合论进行证明,假设从一个树开始,如果添加一条边形成一个环,那么就不再是树了。
- 连通性:树是一种连通无环的图,即任意两节点之间都有一条唯一的路径。
2. 深度优先搜索 (DFS)
深度优先搜索的基本步骤:
- 邻接表存图:通过邻接表存储图的结构,即每个节点有一个列表,存储它所有相邻的节点。
- 记录深度:在树的遍历中,记录每个节点到根节点的距离(深度)。
示例代码:
const int N = 100010;
vector<int> G[N];
int n, dis[N];
void dfs(int u, int f) {
for(int i = 0; i < G[u].size(); i++) {
int v = G[u][i];
if(v == f) continue;
dis[v] = dis[u] + 1;
dfs(v, u);
}
}
解析:
- NNN 是最大节点数。
- GGG 是邻接表,用来存储图。
dis[u]记录节点 uuu 的深度。dfs(u, f)以节点 uuu 为起点,fff 为父节点进行深度优先搜索。
3. 广度优先搜索 (BFS)
广度优先搜索的基本步骤:
- 邻接表存图:同样使用邻接表存图。
- 记录深度:通过一个队列来依次遍历图的节点,并记录每个节点到起点的距离。
示例代码:
const int N = 100010;
vector<int> G[N];
int n, dis[N], fa[N];
queue<int> q;
void bfs(int u) {
q.push(u);
while(!q.empty()) {
int u = q.front();
q.pop();
for(int i = 0; i < G[u].size(); i++) {
int v = G[u][i];
if(v == fa[u]) continue;
fa[v] = u;
dis[v] = dis[u] + 1;
q.push(v);
}
}
}
解析:
- fa[u]fa[u]fa[u] 记录节点 uuu 的父节点。
- dis[u]dis[u]dis[u] 记录节点 uuu 的距离。
- BFS 通过队列从源节点开始层层扩展,访问每个节点的相邻节点。
4. 连通点集
定义:
对于每个节点,能通过其根节点向外扩展形成的子树数量,自己作为一个点也算一个子树。
示例代码:
void dfs1(int x, int fa) {
for(int son : to[x]) {
if(son == fa) continue;
dfs1(son, x);
dp1[x] = (dp1[x] * (dp1[son] + 1)) % mod;
}
}
解析:
- dp1[x]dp1[x]dp1[x] 表示以节点 xxx 为根的子树数。
- 通过遍历每个儿子节点的子树数,累乘得到当前节点的子树数量。
5. 树的直径
定义:
树的直径是树上任意两节点之间最长的路径。
直径的性质:
- 直径的两端点是叶子节点。
- 从树上某个节点出发,找到距离最远的节点,这个节点一定是直径的一个端点。
示例代码:
const int N = 1e4 + 10;
int n, d = 0;
int d1[N], d2[N];
vector<int> e[N];
void dfs(int u, int fa) {
d1[u] = d2[u] = 0;
for(int v : e[u]) {
if(v == fa) continue;
dfs(v, u);
int t = d1[v] + 1;
if(t > d1[u]) {
d2[u] = d1[u], d1[u] = t;
} else if(t > d2[u]) {
d2[u] = t;
}
}
d = max(d, d1[u] + d2[u]);
}
解析:
- d1[u]d1[u]d1[u] 为节点 uuu 向下的最长路径。
- d2[u]d2[u]d2[u] 为节点 uuu 向下的次长路径。
- ddd 为树的直径,即通过每个节点的最长路径和次长路径之和。
6. 两次 DFS 解法
解法步骤:
- 首先从任意一个节点 uuu 开始,找到最远的节点 ccc,这一步的目的是找出树的一个端点。
- 然后从节点 ccc 开始,进行一次 DFS,找到最远的节点,得到树的直径。
示例代码:
const int N = 1e4 + 10;
int n, c = 0;
int d[N];
vector<int> e[N];
void dfs(int x, int fa) {
for(int v : e[x]) {
if(v == fa) continue;
d[v] = d[x] + 1;
if(d[v] > d[c]) c = v;
dfs(v, x);
}
}
void solve() {
cin >> n;
for(int i = 1; i < n; i++) {
int u, v;
cin >> u >> v;
e[u].push_back(v);
e[v].push_back(u);
}
dfs(1, 0);
d[c] = 0;
dfs(c, 0);
cout << d[c] << endl;
}
解析:
- 通过两次 DFS,首先从任意节点开始,找到一个远点,然后从该远点出发找到最大距离,得到树的直径。
7. 树的重心
定义:
树的重心是指把树分成两部分时,某个点是两部分之间的平衡点,使得每一部分的节点数不超过整个树的一半。
示例代码:
const int N = 1e5 + 10;
int sz[N], w[N], cid[2];
int h[N], e[N], ne[N], idx;
int n;
void add(int u, int v) {
e[++idx] = v, ne[idx] = h[u], h[u] = idx;
}
void dfs(int x, int fa) {
sz[x] = 1;
w[x] = 0;
for(int i = h[x]; i != -1; i = ne[i]) {
int v = e[i];
if(v != fa) {
dfs(v, x);
sz[x] += sz[v];
w[x] = max(w[x], sz[v]);
}
}
w[x] = max(w[x], n - sz[x]);
if(w[x] <= n / 2) {
cid[cid[0] != 0] = x;
}
}
解析:
- sz[x]sz[x]sz[x] 为节点 xxx 的子树大小。
- w[x]w[x]w[x] 为节点 xxx 作为根时,最大子树的大小。
- 通过 DFS 计算每个节点的子树大小和最大子树大小,找出重心。
8. 树上的路径长度
问题描述:
计算树上长度恰好为 KKK 的路径数。
解法:
使用树的 DP 动态规划解法,记录每个节点到其他节点的距离,统计路径长度为 KKK 的路径。
示例代码:
const int N = 50010;
struct edge {
int v, ne;
} e[N * 2];
int h[N], idx;
int f[N][510];
int n, m;
ll res = 0;
void add(int u, int v) {
e[++idx] = {v, h[u]};
h[u] = idx;
}
void dfs(int u, int fa) {
f[u][0] = 1;
for(int i = h[u]; i; i = e[i].ne) {
int v = e[i].v;
if(v == fa) continue;
dfs(v, u);
for(int j = 0; j < m; j++) {
res += f[v][j] * f[u][m - j - 1];
}
for(int j = 0; j < m; j++) {
f[u][j + 1] += f[v][j];
}
}
}
解析:
- 通过树的 DFS 动态规划计算路径长度,f[u][j]f[u][j]f[u][j] 表示从节点 uuu 到深度为 jjj 的路径数,最终结果为路径长度为 KKK 的路径数。
通过以上解析,可以看出树相关的图论算法在解决很多复杂问题时非常高效,并且应用广泛。

浙公网安备 33010602011771号