[GESP202312 八级] 大量的工作沟通

GitHub跳转原题关系图返回列表

一组员工都能被同一人管理,等价于这个人是他们的公共祖先;先求这组点的 LCA,再在根到该 LCA 的路径上取最大编号即可。

OJ: luogu

题目 ID: P10113

难度:普及+/提高

标签:LCA倍增树形结构

日期: 2026-06-20 02:32

题意

公司组织结构是一棵以 0 为根的树:

  • 0 号员工是老板
  • 其余每个员工都有一个直接领导

员工 x 能管理员工 y,当且仅当:

  • x = y
  • 或者 xy 的祖先

每次给出一组要合作的员工,要求找一个主持人,使得他能管理这组里的所有人。

如果满足条件的人有很多个,要输出:

  • 编号最大的那个

样例树

样例一的树结构是:

digraph G {
  0 -> 1;
  0 -> 2;
  2 -> 3;
  2 -> 4;
}

对于员工集合 {3,4}

  • 公共祖先有 20
  • 其中编号更大的是 2

所以答案是 2

思路

先看一个最直接的暴力:

cpp
// brute.cpp:直接枚举谁来主持,再暴力判断他是不是所有参与者的祖先。
// 这个做法最贴近题意,但每次查询都要逐个试人,只适合小数据。
#include <bits/stdc++.h>
using namespace std;

int n, q;
vector<int> parent_arr;

bool is_ancestor(int anc, int u) {
    while (true) {
        if (u == anc) {
            return true;
        }
        if (u == 0) {
            break;
        }
        u = parent_arr[u];
    }
    return anc == 0;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n;
    parent_arr.assign(n, 0);

    for (int i = 1; i < n; i++) {
        cin >> parent_arr[i];
    }

    cin >> q;
    while (q--) {
        int m;
        cin >> m;

        vector<int> people(m);
        for (int i = 0; i < m; i++) {
            cin >> people[i];
        }

        int answer = 0;
        for (int cand = 0; cand < n; cand++) {
            bool ok = true;
            for (int i = 0; i < m; i++) {
                if (!is_ancestor(cand, people[i])) {
                    ok = false;
                    break;
                }
            }
            if (ok) {
                answer = max(answer, cand);
            }
        }

        cout << answer << '\n';
    }

    return 0;
}

暴力做法是:

  1. 枚举谁来当主持人
  2. 逐个判断他是不是所有参与者的祖先
  3. 在所有合法人里取编号最大

这个办法很直观,但一次查询最坏要把整棵树都扫一遍。

关键观察有两层。

第一层:

  • 能管理所有参与者的人,必须是这组点的公共祖先

第二层:

  • 所有公共祖先,恰好就是“这组点的 LCA 到根这条链上的所有点”

因为:

  • 最低的那个公共祖先就是这组点的 LCA
  • 比它更高的祖先也都能管理所有人
  • 而 LCA 以下的人不可能同时管理不同分支上的参与者

所以题目并不是“输出 LCA”,而是:

  1. 先求出这一组员工的 LCA
  2. 再在“根到 LCA”的路径上找编号最大的点

第二步也很容易预处理。 设:

  • path_max_id[u] 表示从根走到 u 这条路径上的最大编号

那么每次查询的答案就是:

  • path_max_id[lca(这一组人)]

于是整题就变成了标准的倍增 LCA 预处理,再把一组点不断两两合并成一个总 LCA。

代码

cpp
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 100000 + 5;
const int LOG = 18;

int n, q;
vector<int> child[MAXN];
int depth_arr[MAXN];
int up[MAXN][LOG];
int path_max_id[MAXN];

void init_tree(int n) {
    for (int i = 0; i < n; i++) {
        child[i].clear();
        depth_arr[i] = 0;
        path_max_id[i] = 0;
        for (int j = 0; j < LOG; j++) {
            up[i][j] = 0;
        }
    }
}

void build_lca() {
    vector<int> st;
    st.push_back(0);

    while (!st.empty()) {
        int u = st.back();
        st.pop_back();

        for (size_t i = 0; i < child[u].size(); i++) {
            int v = child[u][i];
            depth_arr[v] = depth_arr[u] + 1;
            up[v][0] = u;
            path_max_id[v] = max(path_max_id[u], v);

            for (int j = 1; j < LOG; j++) {
                up[v][j] = up[up[v][j - 1]][j - 1];
            }

            st.push_back(v);
        }
    }
}

int kth_ancestor(int u, int k) {
    for (int j = 0; j < LOG; j++) {
        if (k & (1 << j)) {
            u = up[u][j];
        }
    }
    return u;
}

int lca(int a, int b) {
    if (depth_arr[a] < depth_arr[b]) {
        swap(a, b);
    }

    a = kth_ancestor(a, depth_arr[a] - depth_arr[b]);
    if (a == b) {
        return a;
    }

    for (int j = LOG - 1; j >= 0; j--) {
        if (up[a][j] != up[b][j]) {
            a = up[a][j];
            b = up[b][j];
        }
    }

    return up[a][0];
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n;
    init_tree(n);

    for (int i = 1; i < n; i++) {
        int f;
        cin >> f;
        child[f].push_back(i);
    }

    path_max_id[0] = 0;
    build_lca();

    cin >> q;
    while (q--) {
        int m;
        cin >> m;

        int cur_lca;
        cin >> cur_lca;

        for (int i = 2; i <= m; i++) {
            int x;
            cin >> x;
            cur_lca = lca(cur_lca, x);
        }

        // 能主持所有人的员工,等价于“所有参与者公共祖先”。
        // 在这些公共祖先里取编号最大值,就是根到 LCA 路径上的最大编号。
        cout << path_max_id[cur_lca] << '\n';
    }

    return 0;
}

复杂度

设树有 n 个点,一次查询有 m 个参与者。

  • 预处理倍增祖先表:O(nlogn)O(n log n)
  • 单次两点 LCA:O(logn)O(log n)
  • 一次查询把 m 个点不断合并:O(mlogn)O(m log n)

空间复杂度:

  • O(nlogn)O(n log n)

总结

这题最容易走偏的地方是:

  • 不是“编号最大的公共祖先 = LCA”

LCA 只是“最深的公共祖先”,而题目要的是“编号最大的公共祖先”。

真正正确的转化是:

  1. 先用 LCA 把“公共祖先集合”压成一条链
  2. 再用 path_max_id 在这条链上取最大编号

所以这是一道:

  • 倍增 LCA
  • 加一个很自然的路径前缀最大值

的组合题。

一图流解析

这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。

一图流解析