🌳 P11962 树上漫步

GESP 2025年3月 六级 · 互动教学
树的遍历 二分图染色 奇偶性 O(n) 优化

📖 第一关:读懂题目

题目大意:小 A 有一棵 n 个结点的树。从某个结点出发,每一步移动到相邻结点,只在偶数步(含 0 步)后结束。问:从每个结点出发,能结束漫步的结点有多少个?

输入:n 和 n-1 条边
输出:n 个整数,第 i 个表示从结点 i 出发的答案
数据范围:n ≤ 2×10⁵,时间 1s,内存 512MB
🔍 把题目翻译成人话:
你站在树上某个结点,可以走来走去(能走重复的路),但最后停下来的那一步必须是偶数步(0、2、4、6...)。问你能停在哪些结点上?
💡 关键信息:
① 这是一棵(连通、无环、n-1 条边)
② 可以走重复的结点和边
③ 只关心偶数步能否到达
④ 对每个结点都要算答案

🤔 先想想暴力怎么做?

最直接的想法:对每个结点做一次 BFS,算出它到所有其他结点的距离,数一下偶数距离的有多少个。时间复杂度 O(n²)。

⚠️ 但是!n ≤ 2×10⁵,O(n²) = 4×10¹⁰,1 秒内跑不完,会 超时 TLE
我们需要一个更聪明的 O(n) 方法。

💡 第二关:发现核心规律

🌲 树的一个重要性质:二分图

树是一种特殊的图——它一定是二分图。什么意思呢?我们可以把树上的所有结点染成两种颜色,使得每条边连接的两个结点颜色不同

🎯 染色规则:从任意结点开始 BFS/DFS,
• 起点染 紫色(偶数层)
• 它的邻居染 绿色(奇数层)
• 邻居的邻居又染 紫色...
交替染色,就像棋盘一样!

📐 偶数步 = 同色结点

这是本题最核心的推理:

推理链条:
① 在树上,两个结点之间有唯一路径(树没有环)
② 路径长度 = 两结点的距离
③ 每走一步,颜色就变一次(因为相邻结点颜色不同)
④ 走偶数步 → 颜色变偶数次 → 回到同色
⑤ 走奇数步 → 颜色变奇数次 → 变成异色

结论:从结点 i 出发,偶数步能到达的 = 所有和 i 同色的结点!
🤔 等等,如果距离是奇数,我多走几步绕回来不就行了?
好问题!在树上你可以走到某个邻居再走回来(+2步),所以奇数距离的结点可以变成奇+2、奇+4... 但永远是奇数!
同理,偶数距离的结点可以变成偶+2、偶+4... 永远是偶数!
所以奇偶性不会因为绕路而改变。
🏆 最终结论:
设紫色(偶数层)结点有 E 个,绿色(奇数层)结点有 O 个。
• 紫色结点的答案 = E(同色都是偶数步可达)
• 绿色结点的答案 = O(同色都是偶数步可达)
只需要一次 BFS/DFS 染色,数一下每种颜色有多少个就行!时间 O(n)!

🎨 第三关:BFS 染色动画

让我们用样例 2 的树来演示染色过程。从结点 1 开始 BFS,交替染成紫色和绿色。

未访问
偶数层(紫)
奇数层(绿)
当前处理
// BFS 染色
int color[N];
int cnt[2]; // cnt[0]=偶数层, cnt[1]=奇数层
queue<int> q;

// 从结点1开始
q.push(1);
color[1] = 0; // 0=紫色(偶数层)
cnt[0]++;

while (!q.empty()) {
  int u = q.front();
  q.pop();
  for (int v : adj[u]) {
    if (color[v] == -1) {
      color[v] = color[u] ^ 1;
      cnt[color[v]]++;
      q.push(v);
    }
  }
}

// 输出答案
for (int i = 1; i <= n; i++)
  cout << cnt[color[i]] << ' ';
点击「播放」开始 BFS 染色动画
BFS 队列:
偶数层(紫):0
奇数层(绿):0
当前步:0
速度: 中速

📐 第四关:奇偶距离可视化

选一个起点,看看树上每个结点到它的距离是奇还是偶。你会发现:同色 = 偶数距离,异色 = 奇数距离

🎮 操作方式:点击树上的任意结点选为起点,系统会自动计算并显示每个结点到起点的距离和奇偶性。
点击树上任意结点,查看距离奇偶性分布

📋 距离表

💻 第五关:C++ 代码逐行讲解

📄 点击展开完整 AC 代码
/*
 * P11962 [GESP202503 六级] 树上漫步
 * 核心思路:BFS二分图染色,同色=偶数步可达
 * 时间复杂度 O(n)
 */
#include <bits/stdc++.h>
using namespace std;

const int N = 200005;
vector<int> adj[N];  // 邻接表存树
int color[N];        // 染色:0=偶数层, 1=奇数层
int cnt[2];           // cnt[0]=偶数层个数, cnt[1]=奇数层个数

int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    
    int n;
    cin >> n;
    
    // 读入 n-1 条边,建邻接表
    for (int i = 0; i < n - 1; i++) {
        int u, v;
        cin >> u >> v;
        adj[u].push_back(v);
        adj[v].push_back(u);
    }
    
    // 初始化:所有结点未染色
    memset(color, -1, sizeof(color));
    
    // BFS 从结点 1 开始染色
    queue<int> q;
    q.push(1);
    color[1] = 0;  // 结点1是第0层(偶数)
    cnt[0]++;      // 偶数层+1
    
    while (!q.empty()) {
        int u = q.front();
        q.pop();
        for (int v : adj[u]) {
            if (color[v] == -1) {  // 未染色
                color[v] = color[u] ^ 1;  // 异或1:0变1, 1变0
                cnt[color[v]]++;   // 对应颜色计数+1
                q.push(v);
            }
        }
    }
    
    // 输出每个结点的答案
    for (int i = 1; i <= n; i++) {
        cout << cnt[color[i]] << " ";
        // color[i]=0 → 输出偶数层个数
        // color[i]=1 → 输出奇数层个数
    }
    cout << endl;
    
    return 0;
}

🔑 关键代码逐行解读

第 1 步:建图
adj[u].push_back(v); adj[v].push_back(u);
树是无向图,每条边两个方向都要存。用 vector 邻接表,比二维数组省空间。
第 2 步:初始化
memset(color, -1, sizeof(color));
把所有结点的颜色设为 -1(未染色)。这样 BFS 时只需检查 color[v] == -1 就知道是否访问过。
第 3 步:BFS 染色(最核心!)
color[v] = color[u] ^ 1; —— 这行是灵魂!

^ 1 是异或运算:
0 ^ 1 = 1(偶数层 → 奇数层)
1 ^ 1 = 0(奇数层 → 偶数层)

父结点的颜色异或 1 就是子结点的颜色。一行代码完成交替染色!
同时 cnt[color[v]]++ 把对应颜色的计数器 +1。
第 4 步:输出答案
cout << cnt[color[i]] << " ";
结点 i 的颜色是 color[i],直接输出该颜色的总数即可。
• 如果 i 是紫色(color=0),答案 = cnt[0](所有紫色结点数)
• 如果 i 是绿色(color=1),答案 = cnt[1](所有绿色结点数)
不需要对每个结点单独算!只需要两个计数器!
⚡ 为什么是 O(n)?
BFS 每个结点入队出队各一次,每条边被检查两次(两个方向),总共 O(n + n) = O(n)。
n = 2×10⁵ 时只需约 0.01 秒,远快于 O(n²) 的暴力!

🎬 第六关:样例推演

样例 1:3 个结点

边:1-3, 2-3 → 树形结构:1 — 3 — 2
1 偶(紫) 3 奇(绿) 2 偶(紫)
结点颜色同色结点答案
1紫(偶)1, 22
2紫(偶)1, 22
3绿(奇)31
输出:2 2 1 ✅ 与样例一致!

样例 2:4 个结点

边:1-3, 3-2, 4-3 → 树形结构:1 — 3, 3 — 2, 3 — 4
1 偶(紫) 3 奇(绿) 2 偶(紫) 4 偶(紫)
结点颜色同色结点答案
1紫(偶)1, 2, 43
2紫(偶)1, 2, 43
3绿(奇)31
4紫(偶)1, 2, 43
输出:3 3 1 3 ✅ 与样例一致!
🎯 观察规律:所有紫色结点的答案都一样(= 紫色总数),所有绿色结点的答案也都一样(= 绿色总数)。
因为答案只取决于颜色,不取决于具体是哪个结点!

🎯 第七关:互动测验

来检验一下你的理解吧!共 6 道题。