树上漫步

GESP 2025年3月六级 · P11962 · 用动画搞懂深度奇偶性

偶数深度 奇数深度 BFS 求深度 O(n) 做法

题目解读:小A在树上漫步

小A有一棵 n 个节点的树,节点编号 1 到 n。小A会从某个节点出发,每一步走到相邻的节点,并且只在偶数步(包括零步)后结束漫步。

题目要求:对每个节点,求出从它出发,经过偶数步能到达的节点有多少个。(可以经过重复的节点。)

关键信息提取

1. :n 个节点,n-1 条边,连通无环

2. 漫步:每步移动到相邻节点,可以重复经过

3. 偶数步结束:0 步、2 步、4 步……都算偶数步

4. 对每个节点求答案:不是只求一个节点

样例 1 分析

输入一棵 3 个节点的树:1 - 3 - 2(3 是中间节点)

偶数步可达
仅奇数步可达
手动分析

从节点 1 出发:0步→在1(偶数✓);1步→到3(奇数);2步→到1或2(偶数✓)。所以偶数步能到 {1, 2},共 2 个。

从节点 2 出发:0步→在2(偶数✓);1步→到3(奇数);2步→到1或2(偶数✓)。所以偶数步能到 {1, 2},共 2 个。

从节点 3 出发:0步→在3(偶数✓);1步→到1或2(奇数);2步→回到3(偶数✓)。所以偶数步能到 {3},共 1 个。

答案:2 2 1

发现规律了吗?

节点 1 和 2 的答案相同(都是 2),节点 3 的答案不同(是 1)。这和节点在树上的"位置"有关——具体来说,和节点的深度奇偶性有关!接下来我们来探索这个奥秘。

偶数步的奥秘:每走一步,奇偶性翻转

我们先在一条简单的路径上观察规律。点击"播放",看看从节点 1 出发,走 0 步、1 步、2 步……分别能到达哪些节点。

点击"播放"开始动画。观察每一步能到达的节点!
偶数步可达
仅奇数步可达
当前步正在扩展
未到达
发现了什么?

每走一步,你移动到相邻节点。而相邻节点的"距离奇偶性"和当前节点相反——所以每走一步,奇偶性就翻转一次!

这意味着:偶数步后,你的距离奇偶性和出发时相同奇数步后,奇偶性翻转了。

而且,你总可以在一条边上"走过去再走回来",浪费 2 步。所以如果距离是 k,你能用 k, k+2, k+4……步到达。能偶数步到达 ⟺ 距离是偶数

深度奇偶性:问题的钥匙

现在我们把规律从"路径"推广到"树"。核心问题是:树上两个节点之间的距离,和它们的深度有什么关系?

什么是深度?

选一个根节点(比如节点 1),深度就是从根到该节点的边数。根的深度是 0,它的孩子深度是 1,孙子深度是 2……

1
每步改变深度 ±1
在树上行走时,每一步要么走向父节点(深度 -1),要么走向子节点(深度 +1)。
2
步数 = 上升步数 + 下降步数
从 u 走到 v,总共走了 up + down 步。其中 up 步是向根方向走,down 步是向叶方向走。
3
深度差 = 下降步数 - 上升步数
depth[v] - depth[u] = down - up。所以 depth[v] = depth[u] + (down - up)。
4
步数 - 深度差 = 2 × 上升步数
(up + down) - (down - up) = 2 × up,一定是偶数
所以:步数 ≡ 深度差 (mod 2)
5
结论
偶数步到达 ⟺ 深度差为偶数 ⟺ depth[u] 和 depth[v] 奇偶性相同
从 u 出发,偶数步能到达的节点 = 所有和 u 深度奇偶性相同的节点!

动画演示:BFS 求深度 + 奇偶性染色

点击"播放",看 BFS 如何从根节点出发,逐层计算每个节点的深度,并按奇偶性染色。

点击"播放"开始 BFS,计算每个节点的深度!
BFS 队列
统计结果
偶数深度: 0 · 奇数深度: 0
偶数深度
奇数深度
当前 BFS 访问
在队列中
未访问
最终答案

BFS 完成后,统计出偶数深度有 a 个节点,奇数深度有 b 个节点(a + b = n)。

对于每个节点 i:如果 depth[i] 是偶数,答案 = a;如果 depth[i] 是奇数,答案 = b

就这么简单!不需要对每个节点单独搜索,只需要一次 BFS就够了。

算法与代码

三步走

算法步骤

第一步:从节点 1 出发,BFS 遍历整棵树,计算每个节点的深度。

第二步:统计偶数深度节点数 cnt[0] 和奇数深度节点数 cnt[1]。

第三步:对每个节点 i,输出 cnt[depth[i] % 2]。

C++ · 完整代码
1#include <bits/stdc++.h>
2using namespace std;
3
4const int MAXN = 200005;
5vector<int> adj[MAXN];  // 邻接表存树
6int depth[MAXN];          // 每个节点的深度
7int cnt[2];               // cnt[0]=偶数深度数, cnt[1]=奇数深度数
8
9void bfs(int root, int n) {
10    queue<int> q;
11    q.push(root);
12    depth[root] = 0;       // 根节点深度为 0
13    bool vis[MAXN] = {};
14    vis[root] = true;
15
16    while (!q.empty()) {
17        int u = q.front(); q.pop();
18        cnt[depth[u] % 2]++;  // 统计奇偶深度
19
20        for (int v : adj[u]) {
21            if (!vis[v]) {
22                vis[v] = true;
23                depth[v] = depth[u] + 1;  // 深度 = 父节点深度 + 1
24                q.push(v);
25            }
26        }
27    }
28}
29
30int main() {
31    ios::sync_with_stdio(false);
32    cin.tie(nullptr);
33
34    int n;
35    cin >> n;
36
37    // 读入 n-1 条边,建树
38    for (int i = 0; i < n - 1; i++) {
39        int u, v;
40        cin >> u >> v;
41        adj[u].push_back(v);
42        adj[v].push_back(u);
43    }
44
45    bfs(1, n);  // 从节点 1 开始 BFS
46
47    // 输出每个节点的答案
48    for (int i = 1; i <= n; i++) {
49        cout << cnt[depth[i] % 2];
50        if (i < n) cout << ' ';
51    }
52    cout << '\n';
53
54    return 0;
55}
代码解读

第 9-28 行 bfs():标准 BFS 模板。从根节点出发,逐层扩展。每访问一个节点,记录深度并统计奇偶。关键是 depth[v] = depth[u] + 1——BFS 天然保证先访问的深度小。

第 18 行 cnt[depth[u] % 2]++:一行代码搞定统计!depth[u] % 2 为 0 就是偶数深度,为 1 就是奇数深度。

第 49 行 cnt[depth[i] % 2]:输出答案。深度奇偶性相同 → 同一组 → 答案就是该组的人数。

复杂度分析

时间复杂度:O(n)。BFS 每个节点和每条边只访问一次。n ≤ 2×10⁵,轻松通过。

空间复杂度:O(n)。邻接表存 n-1 条边,加上 depth 数组和队列。

如果用 O(n²) 暴力(对每个节点单独 BFS),n = 2×10⁵ 时需要 4×10¹⁰ 次运算,会超时!所以深度奇偶性这个规律非常关键。

验证样例

样例 2 验证

输入:4 个节点,边为 1-3, 3-2, 4-3(节点 3 是中心)

以节点 1 为根,BFS 得到深度:depth[1]=0, depth[3]=1, depth[2]=2, depth[4]=2

偶数深度(0, 2):节点 1, 2, 4 → cnt[0] = 3

奇数深度(1):节点 3 → cnt[1] = 1

答案:节点 1 → 3, 节点 2 → 3, 节点 3 → 1, 节点 4 → 3

输出:3 3 1 3

互动练习场:建树求答案

自己建一棵树,看看深度奇偶性染色和每个节点的答案!

操作模式

操作

模式:添加节点。点击空白处创建节点。

知识检测小测验

做完上面的学习,来测测你掌握了多少!共 5 道选择题。