1 条题解
-
0
E. 黑白树 详细题解
问题重述
给定一棵 个节点的树,每个节点是黑色()或白色(),至少有 个黑点。棋子在某个节点上,每次操作可以选择一个黑色顶点 ,然后将棋子沿着从当前位置到 的最短路径的第一条边移动。连续两次操作不能选择同一个黑色顶点。当棋子移动到黑色顶点时结束。对于每个起始节点,判断是否存在操作序列使得棋子最终能到达黑色顶点。
关键观察
观察 1:操作的本质
从节点 移动到相邻节点 是可行的,如果存在一个黑色顶点使得路径经过 且满足条件。实际上,我们可以将问题转化为:在什么情况下可以从 沿着边 移动?
观察 2:可行移动的条件
考虑从 沿着边 移动。设删除边 后, 所在的连通分量为 , 所在的连通分量为 。从 到 的移动是可行的,如果满足以下任一条件:
- 是黑色节点:移动后直接到达黑点,游戏结束,不需要下一次操作
- 中至少有 个黑点:可以用其中一个黑点作为本次移动的目标,另一个作为下次移动的目标(避免连续使用同一黑点)
注意:条件只依赖于 侧的黑点数量,因为本次移动选择的黑点必须在 中(否则路径不会先经过 )。
观察 3:有向图的构建
根据上述条件,我们可以为每条无向边 构建有向边:
- 可行当且仅当: 或 中的黑点数
- 可行当且仅当: 或 中的黑点数
观察 4:可达性分析
如果存在从节点 到某个黑色节点的有向路径,则 的答案为 。为了方便计算,我们构建反向图(将每条有向边反向),然后从所有黑色节点出发进行 BFS/DFS,能到达的节点即为答案。
算法步骤
-
DFS 预处理:
- 任选根节点(如 ),计算每个子树中的黑点数量
- 记录父节点
-
构建有向图:
- 对于每条边 :
- 如果 是 的子节点(),则 可行当且仅当 或
- 如果 是 的父节点,则 可行当且仅当 或 ( 侧的黑点数 = 总黑点数减去 子树中的黑点数)
- 对于每条边 :
-
反向 BFS:
- 将所有黑色节点加入队列
- 在反向图上 BFS,标记所有可达节点
-
输出结果:
- 输出每个节点是否被标记
时间复杂度
- DFS 预处理:
- 构建有向图:
- BFS:
- 总复杂度:
完整代码
#include<bits/stdc++.h> using namespace std; const int N = 300043; vector<int> g[N]; // 原树 vector<int> g2[N]; // 反向有向图 int cnt[N]; // 子树中的黑点数量 int c[N]; // 节点颜色 (1=黑, 0=白) int par[N]; // 父节点 int used[N]; // 是否可达 // DFS 计算子树黑点数量 void dfs(int x, int p = -1) { par[x] = p; cnt[x] = c[x]; // 先加上自己 for (auto y : g[x]) { if (y != p) { dfs(y, x); cnt[x] += cnt[y]; // 累加子树的黑点 } } } int main() { ios_base::sync_with_stdio(false); cin.tie(0); int n; cin >> n; for (int i = 0; i < n; i++) { cin >> c[i]; } for (int i = 1; i < n; i++) { int x, y; cin >> x >> y; x--; y--; g[x].push_back(y); g[y].push_back(x); } // 以 0 为根进行 DFS dfs(0); // 构建反向有向图 for (int i = 0; i < n; i++) { for (auto j : g[i]) { // 判断 i -> j 是否可行 bool can_move = false; if (i == par[j]) { // j 是 i 的子节点 // i 侧的黑点数 = 总数 - cnt[j] int black_in_i_side = cnt[0] - cnt[j]; if (c[j] == 1 || black_in_i_side > 1) { can_move = true; } } else { // j 是 i 的父节点 // i 侧的黑点数 = cnt[i] if (c[j] == 1 || cnt[i] > 1) { can_move = true; } } if (can_move) { // 反向图:从 j 可以走到 i g2[j].push_back(i); } } } // 多源 BFS(从所有黑色节点出发) queue<int> q; for (int i = 0; i < n; i++) { if (c[i] == 1) { q.push(i); used[i] = 1; } } while (!q.empty()) { int v = q.front(); q.pop(); for (auto to : g2[v]) { if (!used[to]) { used[to] = 1; q.push(to); } } } // 输出结果 for (int i = 0; i < n; i++) { cout << used[i] << " \n"[i == n - 1]; } return 0; }代码说明
-
DFS 预处理:
cnt[x]表示以 为根的子树中的黑点数量par[x]记录父节点,用于判断边的方向
-
可行移动判断:
- 对于边 ,需要根据方向分别判断
- 子节点方向:检查父节点侧的黑点数(总数减去子节点子树)
- 父节点方向:检查子节点侧的黑点数(即子节点子树)
-
反向图 BFS:
- 从所有黑点出发,沿着反向边遍历
- 标记所有能到达的节点
-
输出:
used[i] = 1表示可以从节点 到达黑点
示例验证
以题目样例为例:
8 0 1 0 0 0 0 1 0 边:(8,6), (2,5), (7,8), (6,5), (4,5), (6,1), (7,3)- 黑点:节点 2(索引 1)和节点 7(索引 6)
- 输出:
0 1 1 1 1 0 1 1 - 节点 1(索引 0)无法到达黑点,输出 0
- 节点 6(索引 5)无法到达黑点,输出 0
- 其他节点都可以到达黑点
总结
本题的核心技巧:
- 将移动可行性转化为子树黑点数的条件
- 构建有向图表示可行移动
- 使用反向图 + BFS 计算可达性
- 利用树的性质在 时间内完成所有计算
- 1
信息
- ID
- 6273
- 时间
- 1000ms
- 内存
- 256MiB
- 难度
- 2
- 标签
- 递交数
- 1
- 已通过
- 1
- 上传者