3 条题解

  • 0
    @ 2025-12-9 11:56:09

    这是一个用于生成 GESP 2023年12月 六级真题《工作沟通》测试数据的 C++ 数据生成器。

    该生成器会自动创建 1.in/out10.in/out。 测试数据策略:

    1. 规模分层:覆盖 N=10,50,300N=10, 50, 300 的不同规模。
    2. 特殊树形
      • 链状树 (Line):测试递归深度。
      • 乱序链 (Shuffled Line):这是本题的核心坑点。即树的结构是 0大编号小编号0 \to \text{大编号} \to \text{小编号}。如果只求 LCA 而不往上找最大值,会在这种数据上出错。
      • 星形树 (Star):0 连接所有人。
      • 深树/随机树:综合测试。
    3. 查询构造:覆盖 m=2m=2m=Nm=N 的各种查询规模。

    C++ 数据生成器代码

    /**
     * P10109 [GESP202312 六级] 工作沟通 - 数据生成器
     * 功能:生成 1.in/1.out ~ 10.in/10.out
     * 包含:标准解法逻辑 + 针对性数据构造
     */
    
    #include <iostream>
    #include <fstream>
    #include <vector>
    #include <algorithm>
    #include <numeric>
    #include <random>
    #include <string>
    #include <ctime>
    
    using namespace std;
    
    // ==========================================
    // Part 1: 标准题解逻辑 (用于生成 .out)
    // ==========================================
    int parent_node[305];
    int depth[305];
    
    int get_depth_solve(int u) {
        if (u == 0) return 0;
        if (depth[u] != -1) return depth[u];
        return depth[u] = get_depth_solve(parent_node[u]) + 1;
    }
    
    int get_lca_solve(int u, int v) {
        if (depth[u] < depth[v]) swap(u, v);
        while (depth[u] > depth[v]) u = parent_node[u];
        if (u == v) return u;
        while (u != v) {
            u = parent_node[u];
            v = parent_node[v];
        }
        return u;
    }
    
    void solve_and_write(int N, const vector<int>& p_array, int Q, const vector<vector<int>>& queries, ofstream& fout) {
        // 初始化解题环境
        // p_array[i] 表示 i 的父亲
        parent_node[0] = -1;
        for (int i = 1; i < N; ++i) {
            parent_node[i] = p_array[i];
        }
        
        for(int i=0; i<N; ++i) depth[i] = -1;
        depth[0] = 0;
        for(int i=1; i<N; ++i) get_depth_solve(i);
    
        for (const auto& q_nodes : queries) {
            int lca = q_nodes[0];
            for (size_t k = 1; k < q_nodes.size(); ++k) {
                lca = get_lca_solve(lca, q_nodes[k]);
            }
            
            // 核心逻辑:从 LCA 往上找最大 ID
            int ans = -1;
            int curr = lca;
            while (curr != -1) {
                ans = max(ans, curr);
                curr = parent_node[curr];
            }
            fout << ans << endl;
        }
    }
    
    // ==========================================
    // Part 2: 数据构造逻辑 (生成 .in)
    // ==========================================
    mt19937 rng((unsigned)time(0));
    
    int randInt(int min, int max) {
        return uniform_int_distribution<int>(min, max)(rng);
    }
    
    void makeData(int id) {
        string inName = to_string(id) + ".in";
        string outName = to_string(id) + ".out";
        ofstream fin(inName);
        ofstream fout(outName);
    
        int N, Q;
        
        // 1. 设定规模
        if (id <= 2) { N = 10; Q = 5; }      // 基础小数据
        else if (id <= 5) { N = 50; Q = 20; } // 中等数据
        else { N = 300; Q = 100; }           // 极限数据
    
        vector<int> p(N); // p[i] 存储 i 的父亲
        
        // 2. 构造树形结构
        if (id == 3) { 
            // 简单链: 0->1->2...
            // 父亲编号总比自己小,最简单的结构
            for (int i = 1; i < N; ++i) p[i] = i - 1;
        } 
        else if (id == 4 || id == 7) { 
            // 【陷阱数据】乱序链 (Shuffled Chain)
            // 构造路径:0 -> (N-1) -> 1 -> 2 -> ...
            // 这样 LCA 可能是 1,但最大编号是路径上的 N-1
            // 这专门用来 Hack 那些只求 LCA 不往上找最大值的代码
            
            // 构造一条 0 -> perm[0] -> perm[1] ... 的链
            vector<int> perm;
            // 把最大编号放在最前面
            perm.push_back(N-1); 
            for(int i=1; i<N-1; ++i) perm.push_back(i);
            
            // 0 -> perm[0]
            p[perm[0]] = 0;
            // perm[i] -> perm[i+1]
            for(size_t i=0; i<perm.size()-1; ++i) {
                p[perm[i+1]] = perm[i];
            }
        } 
        else if (id == 5) {
            // 星形图: 0 是所有人的直接父亲
            for (int i = 1; i < N; ++i) p[i] = 0;
        } 
        else {
            // 随机树 (Random Tree)
            // 逻辑节点 logical_i 挂在 logical_p (0 <= logical_p < logical_i) 下面
            // 然后通过 label_map 映射成真实的乱序 ID
            // 这样保证 0 是根,且无环,且 ID 大小与层级无关
            vector<int> label_map(N);
            iota(label_map.begin(), label_map.end(), 0);
            
            // 保持 0 号永远映射为 0 (题目要求 0 是老板)
            // 其他节点 ID 随机打乱
            if (id >= 6) shuffle(label_map.begin() + 1, label_map.end(), rng);
            
            // 生成逻辑树的父子关系
            vector<int> logical_parent(N);
            for(int i=1; i<N; ++i) {
                // id >= 8 生成比较深的树,测试递归深度
                if (id >= 8) logical_parent[i] = randInt(max(0, i-5), i-1);
                else logical_parent[i] = randInt(0, i-1);
            }
            
            // 转换为真实 ID 的父子关系
            for(int i=1; i<N; ++i) {
                int u = label_map[i];           // 真实子节点 ID
                int f = label_map[logical_parent[i]]; // 真实父节点 ID
                p[u] = f;
            }
        }
    
        // 3. 输出输入文件
        fin << N << endl;
        // 题目输入格式:一行 N-1 个数,依次是 f_1, f_2 ... f_{N-1}
        for (int i = 1; i < N; ++i) {
            fin << p[i] << (i == N - 1 ? "" : " ");
        }
        fin << endl;
    
        // 4. 构造查询
        fin << Q << endl;
        vector<vector<int>> queries;
        for (int i = 0; i < Q; ++i) {
            int m;
            // 构造不同规模的 m
            if (id == 1) m = 2; // 最小询问
            else if (id == 9) m = N; // 全员询问
            else m = randInt(2, min(N, 10)); // 一般询问
            
            // 随机选取 m 个不重复的员工
            vector<int> candidates(N);
            iota(candidates.begin(), candidates.end(), 0);
            shuffle(candidates.begin(), candidates.end(), rng);
            
            vector<int> q_group;
            for(int k=0; k<m; ++k) q_group.push_back(candidates[k]);
            
            queries.push_back(q_group);
            
            // 输出查询
            fin << m;
            for(int x : q_group) fin << " " << x;
            fin << endl;
        }
    
        // 5. 运行标准解法生成 Output
        solve_and_write(N, p, Q, queries, fout);
        
        fin.close();
        fout.close();
        cout << "Generated Case " << id << " (N=" << N << ")" << endl;
    }
    
    int main() {
        for (int i = 1; i <= 10; ++i) {
            makeData(i);
        }
        cout << "All data generated successfully." << endl;
        return 0;
    }
    
    • 0
      @ 2025-12-9 9:48:17

      这是一个符合 NOIP C++14 竞赛标准的题解代码。

      核心解题思路

      1. 树形结构建立:题目给出每个员工(除了0号)的直接领导 fif_i,这实际上构建了一棵以 0 为根的树。
      2. 合法管理者定义:若 xx 管理 yy,则 xx 必须是 yy 的祖先(包含 yy 自己)。要管理一群人,必须是这群人所有人的公共祖先
      3. 最近公共祖先 (LCA):所有公共祖先构成了从“最近公共祖先”到“根节点”的一条路径。
      4. 寻找答案
        • 第一步:求出参与合作的所有员工的 LCA(最近公共祖先)。由于数据范围 N300N \le 300,可以直接使用暴力的“爬山法”求 LCA。
        • 第二步:从求出的 LCA 开始,沿着父节点一直向上走到根节点 0。
        • 第三步:在路径上记录遇到的最大编号,即为所求。

      标准代码 (C++14)

      /**
       * 题目:P10109 [GESP202312 六级] 工作沟通
       * 考点:树的遍历、最近公共祖先(LCA)、模拟
       * 时间复杂度:O(Q * M * N),N <= 300,能够轻松通过
       */
      
      #include <iostream>
      #include <vector>
      #include <algorithm>
      
      using namespace std;
      
      // 数据范围 N <= 300,开大一点点即可
      const int MAXN = 305;
      
      // parent_node[i] 存储 i 的直接领导(父节点)
      int parent_node[MAXN];
      // depth[i] 存储 i 的深度,用于求 LCA
      int depth[MAXN];
      
      // 记忆化搜索计算深度
      // 相比简单的循环,记忆化搜索可以适应任意顺序的节点输入,不用担心拓扑序问题
      int calc_depth(int u) {
          if (u == 0) return 0; // 根节点深度为0
          if (depth[u] != -1) return depth[u]; // 如果已经计算过,直接返回
          
          // 递归计算父亲的深度 + 1
          return depth[u] = calc_depth(parent_node[u]) + 1;
      }
      
      // 朴素法求两个节点的 LCA (最近公共祖先)
      int get_lca(int u, int v) {
          // 1. 确保 u 是深度较深的那一个,方便后面统一操作
          if (depth[u] < depth[v]) swap(u, v);
          
          // 2. 将 u 向上跳,直到和 v 同一层
          while (depth[u] > depth[v]) {
              u = parent_node[u];
          }
          
          // 3. 如果跳到了同一个点,那这个点就是 LCA
          if (u == v) return u;
          
          // 4. 如果不同,两人同时向上跳,直到相遇
          while (u != v) {
              u = parent_node[u];
              v = parent_node[v];
          }
          return u; // 返回相遇点
      }
      
      void solve() {
          int n;
          if (!(cin >> n)) return;
      
          // 初始化:0号是老板,没有上级,标记为 -1 代表根的上面
          parent_node[0] = -1;
          
          // 易错点1:题目输入是 N-1 个整数,依次对应 1号, 2号 ... N-1号员工的领导
          for (int i = 1; i < n; i++) {
              cin >> parent_node[i];
          }
      
          // 初始化深度数组并计算
          for (int i = 0; i < n; i++) depth[i] = -1;
          depth[0] = 0;
          for (int i = 1; i < n; i++) calc_depth(i);
      
          int q;
          cin >> q;
          while (q--) {
              int m;
              cin >> m;
              
              // 读取第一个员工作为当前的 LCA 基准
              int lca_now;
              cin >> lca_now;
              
              // 关键点2:多个节点的 LCA 等于 {当前LCA} 与 {下一个节点} 的 LCA
              // 就像求多个数的最大公约数一样迭代
              for (int i = 1; i < m; i++) {
                  int emp;
                  cin >> emp;
                  lca_now = get_lca(lca_now, emp);
              }
              
              // 此时 lca_now 是这群人里辈分最低的合格管理者
              // 但题目要求的是【编号最大】的合格管理者
              // 合格管理者包括:lca_now 及其所有祖先
              
              int ans = -1;
              int curr = lca_now;
              
              // 从 LCA 向上遍历直到根节点
              while (curr != -1) {
                  ans = max(ans, curr);
                  curr = parent_node[curr];
              }
              
              cout << ans << endl;
          }
      }
      
      int main() {
          // 竞赛常用 IO 优化
          ios::sync_with_stdio(false);
          cin.tie(0);
          
          solve();
          
          return 0;
      }
      

      关键点与易错点注释

      1. 输入对应关系

        • 代码行 for (int i = 1; i < n; i++):题目明确说明输入的 N1N-1 个整数依次是 f1,f2,f_1, f_2, \dots。很多同学会习惯性地以为输入给出了父子连接对 (u,v)(u, v),这里其实是隐式的下标对应。
      2. LCA 的迭代求法

        • {a,b,c}\{a, b, c\} 的 LCA,等价于 get_lca(get_lca(a, b), c)。在循环中不断更新 lca_now 即可。
      3. 题目陷阱(编号最大)

        • 算出 LCA 后不能直接输出!
        • 例如:树结构 3 -> 0(3是0的儿子),查询 {3}。LCA 是 3。但 3 的领导是 0。虽然 3 可以管理自己,但题目可能存在树结构 50 -> 2 -> 0,如果 LCA 是 2,路径是 2->50,那么编号最大的是 50。
        • 修正:本题中父亲的编号不一定比儿子小(题目未保证拓扑序编号),所以必须沿着 parent_node 向上遍历整条路径寻找 max(id)
      4. 复杂度

        • 每次查询最坏情况需要遍历整个树的高度 O(N)O(N)。总复杂度 O(Q×N)O(Q \times N)
        • N=300,Q=100N=300, Q=100,计算量约 3×1043 \times 10^4,远小于 10810^8 的限制,所以朴素做法完全可行,无需倍增优化。
      • 0
        @ 2025-12-9 9:38:31

        你好!我是你的OI教练。今天我们来分析一道 GESP 六级的真题《工作沟通》。

        这道题是典型的树形结构最近公共祖先(LCA, Lowest Common Ancestor, 有时也称作Least Common Ancestor) 问题的结合,但它有一个独特的“小尾巴”——求最大编号,这让它和标准的模板题有所不同。

        我们按照教练的教学模式,一步步拆解这道题。


        1. 读题关键词:你的“雷达”响了吗?

        在读题时,请圈出以下决定解题方向的关键词:

        1. “直接领导 fif_i
          • 这就告诉我们,这 N 个人构成了一个树形结构
          • 0 号员工是根节点(Root),其他员工 ii 的父节点是 fif_i
        2. “管理”
          • 题目定义的“管理”是指:自己、父亲、父亲的父亲……
          • 翻译成树的术语,就是**祖先(Ancestor)**关系。
        3. “主持这场合作……能够管理参与合作的所有同事”
          • 这意味着主持人必须是所有参与者的公共祖先
        4. “编号最大的员工”
          • 这是最大的坑点!
          • 通常我们求公共祖先,都是求“最近公共祖先(LCA)”,也就是辈分最低的那个。
          • 但这里要求的是编号(ID)最大。这个 ID 最大的领导,可能是 LCA 本人,也可能是 LCA 的领导,甚至是 0 号大老板。
          • 结论:我们不能只求出 LCA 就完事,还需要沿着 LCA 往上走,检查通往根节点的路径上,谁的编号最大。

        2. 预备知识点

        要解决这道题,你需要掌握:

        • 树的存储:使用父节点数组 parent[i] 即可,因为我们主要操作是“往上找”。
        • 深度(Depth)计算:为了方便找 LCA,我们需要知道每个节点的深度。
        • 最近公共祖先(LCA)算法
          • 由于 N300N \le 300,数据范围非常小。
          • 不需要使用倍增法(Doubling)或 Tarjan 算法。
          • 只需要使用最朴素的“爬山法”(先把深度统一,再一起往上跳)即可。

        3. 启发式教学:草稿纸上的推演

        假设我们有如下输入(样例 2 的一部分):

        • 关系:1->0, 2->1, 4->2, 5->1, 6->2
        • 树的结构如下:
                0
                |
                1
               / \
              2   5
             / \
            4   6
          
        • 查询:2 4 5 (参与者是 4 和 5)

        第一步:找 LCA

        • 我们在草稿纸上标出 4 和 5。
        • 4 的祖先链:4 -> 2 -> 1 -> 0
        • 5 的祖先链:5 -> 1 -> 0
        • 它们的交集是 {1, 0}。
        • 其中“最近”的(也就是最下面的)是 1。所以 LCA(4, 5) = 1。

        第二步:求最大编号

        • 题目要找“能够管理 4 和 5 的人”,也就是 {1, 0} 这些人。
        • 题目要求“编号最大”。
        • 比较 1 和 0,最大的是 1
        • 输出 1。

        再看一个例子(假设 ID 不按层级分布):

        • 假设树链是:3 -> 100 -> 5 -> 0 (3是底层,0是根)
        • 查询参与者:3
        • LCA 就是 3 自己。
        • 合法的管理者是:3, 100, 5, 0。
        • 虽然 LCA 是 3,但编号最大的是 100
        • 结论验证:必须从 LCA 走到根,记录路径上的最大值。

        4. 核心逻辑梳理

        1. 建树:读入 fif_i,存入 parent 数组。
        2. 预处理深度:从根节点 DFS 或记忆化搜索,计算每个点的 depth。
        3. 处理询问
          • 对于每组查询的 mm 个人,先求出第 1 个人和第 2 个人的 LCA。
          • 拿这个结果去和第 3 个人求 LCA……以此类推,直到求出所有人的 总LCA
          • 小贴士:集合的 LCA 等于两两 LCA 的迭代结果。
        4. 寻找答案
          • 总LCA 开始,通过 parent 数组一步步往上跳,直到跳到根节点(0)。
          • 在跳的过程中,打擂台记录遇到的最大编号 max_id
          • 输出 max_id

        5. 标准代码 (C++14)

        /**
         * 题目:P10109 [GESP202312 六级] 工作沟通
         * 考点:树的遍历、最近公共祖先(LCA)、模拟
         * 时间复杂度:O(Q * N * m),由于 N<=300,完全足够。
         */
        
        #include <iostream>
        #include <vector>
        #include <algorithm>
        
        using namespace std;
        
        const int MAXN = 305;
        int parent_node[MAXN]; // 记录每个节点的父节点
        int depth[MAXN];       // 记录每个节点的深度
        int N, Q;
        
        // 递归求深度(记忆化,防止重复计算)
        int get_depth(int u) {
            if (u == 0) return 0; // 根节点深度为 0
            if (depth[u] != -1) return depth[u]; // 如果算过了直接返回
            return depth[u] = get_depth(parent_node[u]) + 1;
        }
        
        // 朴素法求两点的 LCA
        int get_lca(int u, int v) {
            // 1. 保证 u 比 v 深(或者同深)
            if (depth[u] < depth[v]) swap(u, v);
            
            // 2. 让 u 往上跳,直到和 v 同一层
            while (depth[u] > depth[v]) {
                u = parent_node[u];
            }
            
            // 3. 如果此时重合,那个点就是 LCA
            if (u == v) return u;
            
            // 4. 否则两个点一起往上跳,直到重合
            while (u != v) {
                u = parent_node[u];
                v = parent_node[v];
            }
            return u;
        }
        
        int main() {
            // 优化 I/O
            ios::sync_with_stdio(false);
            cin.tie(0);
        
            cin >> N;
            // 0 号员工是老板,为了方便,设其父节点为 -1
            parent_node[0] = -1; 
            
            // 读入 N-1 个父亲。注意输入格式是依次给出 f_1, f_2 ... f_{N-1}
            for (int i = 1; i < N; ++i) {
                cin >> parent_node[i];
            }
            
            // 初始化并计算深度
            fill(depth, depth + N, -1);
            depth[0] = 0;
            for (int i = 1; i < N; ++i) {
                get_depth(i);
            }
        
            cin >> Q;
            while (Q--) {
                int m;
                cin >> m;
                
                int current_lca;
                cin >> current_lca; // 先读取第一个人作为当前的 LCA 候选
                
                // 依次读取剩下的人,不断更新 LCA
                for (int i = 1; i < m; ++i) {
                    int emp;
                    cin >> emp;
                    current_lca = get_lca(current_lca, emp);
                }
                
                // 此时 current_lca 是所有参与者的最近公共祖先
                // 也就是“管理范围最小”的那个合格领导
                // 但题目要求“编号最大”的合格领导
                
                int max_id = -1;
                int curr = current_lca;
                
                // 从 LCA 往上遍历直到根节点
                while (curr != -1) {
                    if (curr > max_id) {
                        max_id = curr;
                    }
                    curr = parent_node[curr];
                }
                
                cout << max_id << endl;
            }
            return 0;
        }
        

        6. 易错点总结

        1. 输入下标:输入给的是 f1,f2f_1, f_2 \dots,对应的是员工 1, 2... 的父亲。读入循环要从 i=1 开始。
        2. LCA 不是最终答案:千万不要求出 LCA 就直接输出了!一定要记得往上遍历找 ID 最大的。
        3. 数据范围N300N \le 300 是非常小的,所以不需要写复杂的倍增 LCA,朴素写法代码短且不易错。
        4. 初始化:多组询问之间互不影响,但求深度的 depth 数组记得初始化(虽然用记忆化搜索只需一次)。

        希望这个思路能帮你顺利拿下这道题!加油!

        • 1

        信息

        ID
        14430
        时间
        1000ms
        内存
        32MiB
        难度
        3
        标签
        递交数
        1
        已通过
        1
        上传者