3 条题解
-
0
这是一个用于生成 GESP 2023年12月 六级真题《工作沟通》测试数据的 C++ 数据生成器。
该生成器会自动创建
1.in/out到10.in/out。 测试数据策略:- 规模分层:覆盖 的不同规模。
- 特殊树形:
- 链状树 (Line):测试递归深度。
- 乱序链 (Shuffled Line):这是本题的核心坑点。即树的结构是 。如果只求 LCA 而不往上找最大值,会在这种数据上出错。
- 星形树 (Star):0 连接所有人。
- 深树/随机树:综合测试。
- 查询构造:覆盖 到 的各种查询规模。
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
这是一个符合 NOIP C++14 竞赛标准的题解代码。
核心解题思路
- 树形结构建立:题目给出每个员工(除了0号)的直接领导 ,这实际上构建了一棵以 0 为根的树。
- 合法管理者定义:若 管理 ,则 必须是 的祖先(包含 自己)。要管理一群人,必须是这群人所有人的公共祖先。
- 最近公共祖先 (LCA):所有公共祖先构成了从“最近公共祖先”到“根节点”的一条路径。
- 寻找答案:
- 第一步:求出参与合作的所有员工的 LCA(最近公共祖先)。由于数据范围 ,可以直接使用暴力的“爬山法”求 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; }关键点与易错点注释
-
输入对应关系:
- 代码行
for (int i = 1; i < n; i++):题目明确说明输入的 个整数依次是 。很多同学会习惯性地以为输入给出了父子连接对 ,这里其实是隐式的下标对应。
- 代码行
-
LCA 的迭代求法:
- 求 的 LCA,等价于
get_lca(get_lca(a, b), c)。在循环中不断更新lca_now即可。
- 求 的 LCA,等价于
-
题目陷阱(编号最大):
- 算出 LCA 后不能直接输出!
- 例如:树结构
3 -> 0(3是0的儿子),查询{3}。LCA 是 3。但 3 的领导是 0。虽然 3 可以管理自己,但题目可能存在树结构50 -> 2 -> 0,如果 LCA 是 2,路径是2->50,那么编号最大的是 50。 - 修正:本题中父亲的编号不一定比儿子小(题目未保证拓扑序编号),所以必须沿着
parent_node向上遍历整条路径寻找max(id)。
-
复杂度:
- 每次查询最坏情况需要遍历整个树的高度 。总复杂度 。
- ,计算量约 ,远小于 的限制,所以朴素做法完全可行,无需倍增优化。
-
0
你好!我是你的OI教练。今天我们来分析一道 GESP 六级的真题《工作沟通》。
这道题是典型的树形结构与 最近公共祖先(LCA, Lowest Common Ancestor, 有时也称作Least Common Ancestor) 问题的结合,但它有一个独特的“小尾巴”——求最大编号,这让它和标准的模板题有所不同。
我们按照教练的教学模式,一步步拆解这道题。
1. 读题关键词:你的“雷达”响了吗?
在读题时,请圈出以下决定解题方向的关键词:
- “直接领导 ”:
- 这就告诉我们,这 N 个人构成了一个树形结构。
- 0 号员工是根节点(Root),其他员工 的父节点是 。
- “管理”:
- 题目定义的“管理”是指:自己、父亲、父亲的父亲……
- 翻译成树的术语,就是**祖先(Ancestor)**关系。
- “主持这场合作……能够管理参与合作的所有同事”:
- 这意味着主持人必须是所有参与者的公共祖先。
- “编号最大的员工”:
- 这是最大的坑点!
- 通常我们求公共祖先,都是求“最近公共祖先(LCA)”,也就是辈分最低的那个。
- 但这里要求的是编号(ID)最大。这个 ID 最大的领导,可能是 LCA 本人,也可能是 LCA 的领导,甚至是 0 号大老板。
- 结论:我们不能只求出 LCA 就完事,还需要沿着 LCA 往上走,检查通往根节点的路径上,谁的编号最大。
2. 预备知识点
要解决这道题,你需要掌握:
- 树的存储:使用父节点数组
parent[i]即可,因为我们主要操作是“往上找”。 - 深度(Depth)计算:为了方便找 LCA,我们需要知道每个节点的深度。
- 最近公共祖先(LCA)算法:
- 由于 ,数据范围非常小。
- 不需要使用倍增法(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. 核心逻辑梳理
- 建树:读入 ,存入
parent数组。 - 预处理深度:从根节点 DFS 或记忆化搜索,计算每个点的 depth。
- 处理询问:
- 对于每组查询的 个人,先求出第 1 个人和第 2 个人的 LCA。
- 拿这个结果去和第 3 个人求 LCA……以此类推,直到求出所有人的 总LCA。
- 小贴士:集合的 LCA 等于两两 LCA 的迭代结果。
- 寻找答案:
- 从 总LCA 开始,通过
parent数组一步步往上跳,直到跳到根节点(0)。 - 在跳的过程中,打擂台记录遇到的最大编号
max_id。 - 输出
max_id。
- 从 总LCA 开始,通过
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, 2... 的父亲。读入循环要从
i=1开始。 - LCA 不是最终答案:千万不要求出 LCA 就直接输出了!一定要记得往上遍历找 ID 最大的。
- 数据范围: 是非常小的,所以不需要写复杂的倍增 LCA,朴素写法代码短且不易错。
- 初始化:多组询问之间互不影响,但求深度的
depth数组记得初始化(虽然用记忆化搜索只需一次)。
希望这个思路能帮你顺利拿下这道题!加油!
- “直接领导 ”:
- 1
信息
- ID
- 14430
- 时间
- 1000ms
- 内存
- 32MiB
- 难度
- 3
- 标签
- 递交数
- 1
- 已通过
- 1
- 上传者