题解:洛谷 P10113 大量的沟通工作
【题目来源】
洛谷:P10113 [GESP202312 八级] 大量的工作沟通
【题目描述】
某公司有 \(N\) 名员工,编号从 \(0\) 至 \(N-1\)。其中,除了 \(0\) 号员工是老板,其余每名员工都有一个直接领导。我们假设编号为 \(i\) 的员工的直接领导是 \(f_i\)。
该公司有严格的管理制度,每位员工只能受到本人或直接领导或间接领导的管理。具体来说,规定员工 \(x\) 可以管理员工 \(y\),当且仅当 \(x=y\),或 \(x=f_y\),或 \(x\) 可以管理 \(f_y\)。特别地,\(0\) 号员工老板只能自我管理,无法由其他任何员工管理。
现在,有一些同事要开展合作,他们希望找到一位同事来主持这场合作,这位同事必须能够管理参与合作的所有同事。如果有多名满足这一条件的员工,他们希望找到编号最大的员工。你能帮帮他们吗?
【输入】
第一行一个整数 \(N\) ,表示员工的数量。
第二行 \(N-1\) 个用空格隔开的正整数,依次为 \(f_1, f_2, \dots f_{N-1}\)。
第三行一个整数 \(Q\) ,表示共有 \(Q\) 场合作需要安排。
接下来 \(Q\) 行,每行描述一场合作:开头是一个整数 \(m\)(\(2 \leq m \leq N\)),表示参与本次合作的员工数量;接着是 \(m\) 个整数,依次表示参与本次合作的员工编号(保证编号合法且不重复)。
保证公司结构合法,即不存在任意一名员工,其本人是自己的直接或间接领导。
【输出】
输出 \(Q\) 行,每行一个整数,依次为每场合作的主持人选。
【输入样例】
5
0 0 2 2
3
2 3 4
3 2 3 4
2 1 4
【输出样例】
2
2
0
【核心思想】
-
问题分析:给定一棵以 0 号员工为根的有根树,每个员工可以管理其所有后代(包括自己)。对于多组询问,每组给出若干员工编号,需要找到一个编号最大的员工,使得该员工是所有这些员工的共同祖先(即能管理所有参与者)。若存在多个,取编号最大者。由于祖先关系具有传递性,共同祖先即所有节点的最近公共祖先(LCA)及其祖先。因此,问题转化为:
- 求一组节点的 LCA(最近公共祖先)。
- 在从根到该 LCA 的路径上,找出编号最大的节点。
- 由于编号不随深度单调,不能简单取 LCA 本身的编号,需要预处理根到每个节点的路径最大编号。
-
算法选择:
- 倍增法求 LCA:预处理每个节点的倍增祖先表
f[u][k](\(2^k\) 级祖先)和深度dep[u],同时预处理mx[u]表示从根节点到节点u的路径上所有节点编号的最大值。 - 查询过程:对每组询问,依次将每个参与节点与当前 LCA 合并求 LCA(
lca = getLCA(lca, x)),最终得到所有参与节点的 LCAt。答案即为mx[t],因为所有能够管理这些节点的员工正是从根到t路径上的所有节点,编号最大者即为该路径最大值。
- 倍增法求 LCA:预处理每个节点的倍增祖先表
-
关键步骤:
- 读入与建树:读取 \(N\),对于 \(i=1 \dots N-1\),读取
f_i,在树中添加边(f_i, i)。 - DFS 预处理:从根节点 0 开始 DFS,计算每个节点的深度
dep[u]、倍增祖先f[u][k](\(k=0..20\))以及路径最大编号mx[u] = max(mx[parent], u)。 - LCA 函数:使用倍增法求两个节点的最近公共祖先。
- 处理询问:对于每组询问,读取
m和第一个节点,将其作为初始 LCA;然后依次读取剩余节点,不断更新 LCA。最终输出mx[lca]。
- 读入与建树:读取 \(N\),对于 \(i=1 \dots N-1\),读取
-
时间/空间复杂度:
- 预处理:\(O(N \log N)\)。
- 每次询问:\(O(m \log N)\),总复杂度可接受(\(N, Q \le 10^5\))。
- 空间复杂度:\(O(N \log N)\)。
-
树上祖先与路径最大值:
- 管理关系的树形结构:员工之间的管理关系构成一棵树,祖先即管理关系。任意一组员工的共同管理者即为它们的公共祖先。
- 最近公共祖先(LCA):所有公共祖先中深度最大的节点是 LCA,它是管理这些员工所需的最低管理者;所有能管理这些员工的节点是从根到 LCA 路径上的所有节点。
- 路径最大值预处理:由于节点编号不随深度单调,需预先计算根到每个节点的路径最大编号,以便快速回答“路径上最大编号”的查询。
- 适用场景:适用于树形结构中的“最近公共祖先”查询及“路径信息统计”问题,特别适合需要多次查询多节点共同祖先的场景。
【算法标签】
普及 #最近公共祖先
【代码详解】
#include <bits/stdc++.h>
using namespace std;
const int N = 100005; // 最大员工数量
int n, q; // n: 员工总数, q: 合作场次
int f[N][25]; // f[u][i]: u 的 2^i 级祖先(根节点 0 的祖先为 0)
int dep[N]; // dep[u]: u 在树中的深度(根节点深度为 0)
int mx[N]; // mx[u]: 从根节点到 u 的路径上最大的节点编号
vector<int> g[N]; // 邻接表存储树(无向边)
// 深度优先搜索,预处理每个节点的深度、倍增祖先以及路径最大编号
void dfs(int u, int fa)
{
f[u][0] = fa; // 直接父节点
dep[u] = dep[fa] + 1; // 深度 = 父节点深度 + 1
mx[u] = max(mx[fa], u); // 路径上最大编号 = max(父路径最大, 当前节点)
// 倍增祖先预处理
for (int i = 1; i <= 20; i++) // 2^20 > 1e5,足够覆盖 N
{
f[u][i] = f[f[u][i - 1]][i - 1]; // u 的 2^i 级祖先
}
// 递归遍历子节点
for (int v : g[u])
{
if (v == fa)
continue; // 跳过父节点
dfs(v, u);
}
}
// 倍增法求最近公共祖先 (LCA)
int lca(int u, int v)
{
// 确保 u 的深度不小于 v,使 u 位于较深位置
if (dep[u] < dep[v])
swap(u, v);
// 将 u 提升到与 v 同一深度
for (int i = 20; i >= 0; i--)
{
if (dep[f[u][i]] < dep[v])
continue; // 若跳 2^i 步后深度小于 v,则跳过该步
u = f[u][i]; // 向上跳 2^i 步
}
if (u == v)
return u; // v 是 u 的祖先
// 同时提升 u 和 v,直到它们的父节点相同
for (int i = 20; i >= 0; i--)
{
if (f[u][i] == f[v][i])
continue; // 若跳 2^i 步后相遇,则跳过该步(避免越界)
u = f[u][i];
v = f[v][i];
}
return f[u][0]; // 返回父节点即为 LCA
}
int main()
{
cin >> n; // 输入员工总数
// 读入每个员工的直接领导(编号 1 到 n-1)
for (int i = 1; i < n; i++)
{
int x;
cin >> x;
f[i][0] = x; // 记录直接父节点
g[x].push_back(i); // 建无向边
g[i].push_back(x);
}
// 从根节点 0 开始预处理
dfs(0, 0);
cin >> q; // 输入合作场次
while (q--)
{
int m;
cin >> m; // 参与本次合作的员工数
// 读取前两个员工编号,用于初始化 LCA
int a, b;
cin >> a >> b;
int t = lca(a, b); // 当前参与者的 LCA
// 依次与剩余员工合并 LCA
for (int i = 3; i <= m; i++)
{
int x;
cin >> x;
t = lca(t, x); // 更新所有参与者的共同祖先
}
// 能够管理所有参与者的节点为 t 的所有祖先(包括 t)
// 其中编号最大的节点即为 mx[t](根到 t 路径上的最大编号)
cout << mx[t] << endl;
}
return 0;
}
【运行结果】
5
0 0 2 2
3
2 3 4
2
3 2 3 4
2
2 1 4
0
浙公网安备 33010602011771号