题解:洛谷 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

【核心思想】

  1. 问题分析:给定一棵以 0 号员工为根的有根树,每个员工可以管理其所有后代(包括自己)。对于多组询问,每组给出若干员工编号,需要找到一个编号最大的员工,使得该员工是所有这些员工的共同祖先(即能管理所有参与者)。若存在多个,取编号最大者。由于祖先关系具有传递性,共同祖先即所有节点的最近公共祖先(LCA)及其祖先。因此,问题转化为:

    • 求一组节点的 LCA(最近公共祖先)。
    • 在从根到该 LCA 的路径上,找出编号最大的节点。
    • 由于编号不随深度单调,不能简单取 LCA 本身的编号,需要预处理根到每个节点的路径最大编号。
  2. 算法选择

    • 倍增法求 LCA:预处理每个节点的倍增祖先表 f[u][k]\(2^k\) 级祖先)和深度 dep[u],同时预处理 mx[u] 表示从根节点到节点 u 的路径上所有节点编号的最大值。
    • 查询过程:对每组询问,依次将每个参与节点与当前 LCA 合并求 LCA(lca = getLCA(lca, x)),最终得到所有参与节点的 LCA t。答案即为 mx[t],因为所有能够管理这些节点的员工正是从根到 t 路径上的所有节点,编号最大者即为该路径最大值。
  3. 关键步骤

    • 读入与建树:读取 \(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]
  4. 时间/空间复杂度

    • 预处理:\(O(N \log N)\)
    • 每次询问:\(O(m \log N)\),总复杂度可接受(\(N, Q \le 10^5\))。
    • 空间复杂度:\(O(N \log N)\)
  5. 树上祖先与路径最大值

    • 管理关系的树形结构:员工之间的管理关系构成一棵树,祖先即管理关系。任意一组员工的共同管理者即为它们的公共祖先。
    • 最近公共祖先(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
posted @ 2026-09-01 09:25  团爸讲算法  阅读(18)  评论(0)    收藏  举报