3 条题解
-
0
这是一个功能完备的 C++ 数据生成器。它集成了倍增 LCA + 路径最大值预处理的标准解法(用于生成
.out)和针对题目不同测试点要求的数据构造逻辑(用于生成.in)。数据生成策略说明
本生成器生成的 10 个测试点覆盖了以下情况:
- Case 1-2: 小规模数据 (),用于验证基础逻辑。
- Case 3-4: 中规模数据 (),覆盖 50% 数据点要求。
- Case 5: 链状树 (),测试深度和栈空间。
- Case 6: Hack 数据 (),构造“根节点附近有极大编号,而叶子节点编号较小”的特殊链。测试是否正确向上查找最大值,而不是仅仅输出 LCA。
- Case 7: 星形树 (),所有点直接挂在 0 号下面。
- Case 8: 二叉树/随机平衡树 (),测试一般的树形结构。
- Case 9: 大查询压力 (),测试单次询问点数很多的效率。
- Case 10: 综合随机 (),大规模随机树和随机查询。
C++ 数据生成器代码
/** * P10113 [GESP202312 八级] 大量的工作沟通 - 数据生成器 * 功能:生成 1.in/1.out ~ 10.in/10.out */ #include <iostream> #include <fstream> #include <vector> #include <algorithm> #include <cmath> #include <string> #include <random> #include <ctime> #include <numeric> using namespace std; // ========================================== // Part 1: 标准题解逻辑 (用于生成 .out) // ========================================== namespace Solution { const int MAXN = 100005; const int LOGN = 20; vector<int> adj[MAXN]; int up[MAXN][LOGN]; int depth[MAXN]; int max_id[MAXN]; // 路径最大值 void dfs(int u, int p, int d) { depth[u] = d; up[u][0] = p; // 预处理路径最大值 if (p != u) max_id[u] = max(u, max_id[p]); else max_id[u] = u; for (int v : adj[u]) { if (v != p) dfs(v, u, d + 1); } } void init_lca(int n) { // 根的父亲设为自己,防止越界 up[0][0] = 0; for (int j = 1; j < LOGN; j++) { for (int i = 0; i < n; i++) { up[i][j] = up[up[i][j - 1]][j - 1]; } } } int get_lca(int u, int v) { if (depth[u] < depth[v]) swap(u, v); for (int j = LOGN - 1; j >= 0; j--) { if (depth[up[u][j]] >= depth[v]) u = up[u][j]; } if (u == v) return u; for (int j = LOGN - 1; j >= 0; j--) { if (up[u][j] != up[v][j]) { u = up[u][j]; v = up[v][j]; } } return up[u][0]; } void solve(int n, const vector<int>& parents, int q, const vector<vector<int>>& queries, ofstream& fout) { // 清空 for(int i=0; i<n; i++) adj[i].clear(); // 建树 for (int i = 1; i < n; i++) { int p = parents[i-1]; // parents数组下标0对应节点1 adj[p].push_back(i); } // 预处理 max_id[0] = 0; dfs(0, 0, 1); init_lca(n); // 处理询问 for(const auto& group : queries) { if(group.empty()) continue; int lca = group[0]; for(size_t k=1; k<group.size(); k++) { lca = get_lca(lca, group[k]); } fout << max_id[lca] << "\n"; } } } // ========================================== // Part 2: 数据构造工具 // ========================================== mt19937 rng((unsigned)time(0)); int randInt(int min, int max) { return uniform_int_distribution<int>(min, max)(rng); } // ========================================== // Part 3: 测试点生成逻辑 // ========================================== void makeData(int id) { string inName = to_string(id) + ".in"; string outName = to_string(id) + ".out"; ofstream fin(inName); ofstream fout(outName); // 提高 I/O 效率 fin.sync_with_stdio(false); int N, Q; vector<int> parents; // parents[i] 表示 i+1 号节点的父亲 // 1. 设置规模和树形结构 if (id <= 2) { N = (id == 1) ? 10 : 50; Q = 10; // 随机小树 for (int i = 1; i < N; i++) parents.push_back(randInt(0, i - 1)); } else if (id <= 4) { N = 300; Q = 50; // 随机树 for (int i = 1; i < N; i++) parents.push_back(randInt(0, i - 1)); } else if (id == 5) { // 链状树: 0->1->2... N = 100000; Q = 100; for (int i = 1; i < N; i++) parents.push_back(i - 1); } else if (id == 6) { // 【Hack 数据】特殊链: 0 -> (N-1) -> 1 -> 2 -> ... -> (N-2) // 这样底部的节点 LCA 是自己,但往上走会遇到最大的 N-1 N = 100000; Q = 100; // 节点 1 的父亲是 N-1 // 节点 2 的父亲是 1 // ... // 节点 N-1 的父亲是 0 parents.resize(N - 1); parents[0] = N - 1; // f_1 = N-1 for(int i=2; i<N-1; i++) parents[i-1] = i-1; // f_i = i-1 parents[N-2] = 0; // f_{N-1} = 0 } else if (id == 7) { // 星形树: 0 连接所有 N = 100000; Q = 100; for (int i = 1; i < N; i++) parents.push_back(0); } else if (id == 8) { // 较平衡的树 / 随机宽树 N = 100000; Q = 100; for (int i = 1; i < N; i++) parents.push_back(randInt(max(0, i-500), i-1)); } else if (id == 9) { // 大查询压力 N = 100000; Q = 100; // m 将会很大 for (int i = 1; i < N; i++) parents.push_back(randInt(0, i - 1)); } else { // id == 10 // 综合随机 N = 100000; Q = 100; for (int i = 1; i < N; i++) parents.push_back(randInt(0, i - 1)); } // 2. 输出 N 和 父节点数组 fin << N << "\n"; for (int i = 0; i < N - 1; i++) { fin << parents[i] << (i == N - 2 ? "" : " "); } fin << "\n"; fin << Q << "\n"; // 3. 构造并输出查询 vector<vector<int>> all_queries; for (int i = 0; i < Q; i++) { int m; // 针对不同 Case 调整 m 的大小 if (id == 9) m = randInt(1000, 10000); // 大 m else m = randInt(2, min(N, 20)); // 普通 m vector<int> p(N); iota(p.begin(), p.end(), 0); // 如果是 Hack 数据,稍微特殊处理,多查底部节点 if (id == 6) { // 主要是为了查底部,看看能否追溯到 N-1 // 随机打乱 p,取前 m 个 shuffle(p.begin(), p.end(), rng); } else { shuffle(p.begin(), p.end(), rng); } vector<int> q_nodes; for(int k=0; k<m; k++) q_nodes.push_back(p[k]); all_queries.push_back(q_nodes); fin << m; for(int x : q_nodes) fin << " " << x; fin << "\n"; } // 4. 运行标准解法生成 .out Solution::solve(N, parents, Q, all_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; }使用指南
- 编译:
g++ generator.cpp -o generator -O2 - 运行:
./generator(Windows 下为generator.exe) - 结果:当前目录下会生成
1.in~10.in和对应的标准答案1.out~10.out。 - 特别说明:
- Case 6 是最关键的测试点。它的树结构设计为 。如果查询节点 和 的主持人,LCA 是 ,但正确答案(编号最大的管理者)应该是 。这个点可以卡掉只求 LCA 没求路径最大值的代码。
- Case 9 重点测试程序处理大量 的效率。
- 所有 I/O 均使用了
\n而非endl以确保大数据下的生成速度。
上面版本的数据生成器在case4之后会超时
你遇到的问题大概率不是“时间复杂度”导致的超时,而是**栈溢出(Stack Overflow)**导致的程序崩溃或假死。
原因分析: 在生成 Case 5 和 Case 6 时,由于构造的是链状树(一条线),树的深度达到了 。 标准题解代码中使用了
dfs递归进行预处理。在 Windows 等默认栈空间较小(约 2MB)的环境下, 层递归会直接撑爆栈空间,导致程序在生成第 5 个点时瞬间崩溃或停止响应,看起来就像“超时”了。解决方案: 将
Solution命名空间中的 递归 DFS 改为 非递归 BFS(广度优先搜索)。BFS 使用堆内存(queue),可以轻松处理任意深度的树。以下是修复后的完整代码(重点修改了
Solution部分):/** * P10113 [GESP202312 八级] 大量的工作沟通 - 数据生成器 (防爆栈优化版) * 优化:将 DFS 改为 BFS,解决生成链状数据时的栈溢出问题 */ #include <iostream> #include <fstream> #include <vector> #include <algorithm> #include <cmath> #include <string> #include <random> #include <ctime> #include <numeric> #include <queue> // 引入 queue using namespace std; // ========================================== // Part 1: 标准题解逻辑 (非递归优化版) // ========================================== namespace Solution { const int MAXN = 100005; const int LOGN = 20; vector<int> adj[MAXN]; int up[MAXN][LOGN]; int depth[MAXN]; int max_id[MAXN]; // 路径最大值 // 【修改点】使用 BFS 替代 DFS,防止爆栈 void bfs_precalc(int root) { queue<int> q; q.push(root); depth[root] = 1; up[root][0] = root; // 根节点的父亲设为自己,方便倍增边界处理 max_id[root] = root; // 根节点路径最大值是自己 while (!q.empty()) { int u = q.front(); q.pop(); for (int v : adj[u]) { // 因为是根据父子关系建的有向图,不需要判断 v != p depth[v] = depth[u] + 1; up[v][0] = u; // 记录父亲 // 递推路径最大值 max_id[v] = max(v, max_id[u]); q.push(v); } } } void init_lca(int n) { // 倍增预处理 for (int j = 1; j < LOGN; j++) { for (int i = 0; i < n; i++) { up[i][j] = up[up[i][j - 1]][j - 1]; } } } int get_lca(int u, int v) { if (depth[u] < depth[v]) swap(u, v); // 先跳到同一深度 for (int j = LOGN - 1; j >= 0; j--) { if (depth[up[u][j]] >= depth[v]) u = up[u][j]; } if (u == v) return u; // 一起跳 for (int j = LOGN - 1; j >= 0; j--) { if (up[u][j] != up[v][j]) { u = up[u][j]; v = up[v][j]; } } return up[u][0]; } void solve(int n, const vector<int>& parents, int q, const vector<vector<int>>& queries, ofstream& fout) { // 清空邻接表 for(int i=0; i<n; i++) adj[i].clear(); // 建树 (有向图:父 -> 子) for (int i = 1; i < n; i++) { int p = parents[i-1]; adj[p].push_back(i); } // 1. BFS 预处理深度、父亲和 max_id (非递归,防爆栈) bfs_precalc(0); // 2. 倍增表计算 init_lca(n); // 3. 处理询问 for(const auto& group : queries) { if(group.empty()) continue; int lca = group[0]; for(size_t k=1; k<group.size(); k++) { lca = get_lca(lca, group[k]); } fout << max_id[lca] << "\n"; } } } // ========================================== // Part 2: 数据构造工具 // ========================================== mt19937 rng((unsigned)time(0)); int randInt(int min, int max) { return uniform_int_distribution<int>(min, max)(rng); } // ========================================== // Part 3: 测试点生成逻辑 // ========================================== void makeData(int id) { string inName = to_string(id) + ".in"; string outName = to_string(id) + ".out"; ofstream fin(inName); ofstream fout(outName); // 提高 I/O 效率 fin.sync_with_stdio(false); int N, Q; vector<int> parents; // parents[i] 表示 i+1 号节点的父亲 // 1. 设置规模和树形结构 if (id <= 2) { N = (id == 1) ? 10 : 50; Q = 10; for (int i = 1; i < N; i++) parents.push_back(randInt(0, i - 1)); } else if (id <= 4) { N = 300; Q = 50; for (int i = 1; i < N; i++) parents.push_back(randInt(0, i - 1)); } else if (id == 5) { // 链状树: 0->1->2... (递归深度 10^5,原代码会崩) N = 100000; Q = 100; for (int i = 1; i < N; i++) parents.push_back(i - 1); } else if (id == 6) { // Hack 数据: 0 -> (N-1) -> 1 -> 2 ... N = 100000; Q = 100; parents.resize(N - 1); parents[0] = N - 1; for(int i=2; i<N-1; i++) parents[i-1] = i-1; parents[N-2] = 0; } else if (id == 7) { // 星形 N = 100000; Q = 100; for (int i = 1; i < N; i++) parents.push_back(0); } else if (id == 8) { // 较平衡 N = 100000; Q = 100; for (int i = 1; i < N; i++) parents.push_back(randInt(max(0, i-500), i-1)); } else if (id == 9) { // 大查询压力 N = 100000; Q = 100; for (int i = 1; i < N; i++) parents.push_back(randInt(0, i - 1)); } else { // 综合随机 N = 100000; Q = 100; for (int i = 1; i < N; i++) parents.push_back(randInt(0, i - 1)); } // 2. 输出 N 和 父节点数组 fin << N << "\n"; for (int i = 0; i < N - 1; i++) { fin << parents[i] << (i == N - 2 ? "" : " "); } fin << "\n"; fin << Q << "\n"; // 3. 构造并输出查询 vector<vector<int>> all_queries; for (int i = 0; i < Q; i++) { int m; if (id == 9) m = randInt(1000, 10000); else m = randInt(2, min(N, 20)); vector<int> p(N); iota(p.begin(), p.end(), 0); if (id == 6) { shuffle(p.begin(), p.end(), rng); } else { shuffle(p.begin(), p.end(), rng); } vector<int> q_nodes; for(int k=0; k<m; k++) q_nodes.push_back(p[k]); all_queries.push_back(q_nodes); fin << m; for(int x : q_nodes) fin << " " << x; fin << "\n"; } // 4. 运行标准解法生成 .out Solution::solve(N, parents, Q, all_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 竞赛标准的题解代码。
解题思路核心
这道题是 GESP 六级题目《工作沟通》的进阶版。主要区别在于数据范围从 扩大到了 。
-
LCA (最近公共祖先):
- 题目要求找到能管理所有参与者的“主持人”。在树形结构中,能够管理集合 的节点,一定是这些节点的 LCA 以及 LCA 的所有祖先。
- 因此,第一步是求出这 个人的 LCA。
- 由于 高达 ,且询问 和人数 较多,暴力的爬山法(一步步往上跳)在最坏情况下(链状树)会超时。必须使用 倍增法 (Binary Lifting) 求 LCA,将单词查询复杂度优化到 。
-
路径最值预处理 (Prefix Max):
- 题目并不只要求输出 LCA,而是要求输出 LCA 到根节点这一条路径上 编号最大 的节点。
- 这是一个经典的“静态链查询”问题。我们可以利用前缀和的思想,定义
max_id[u]为 从根节点到 u 节点路径上的最大编号。 - 递推公式:
max_id[u] = max(u, max_id[father[u]])。 - 这个数组可以在预处理倍增表时的 DFS/BFS 中一并计算,时间复杂度 。查询时 。
时间与空间复杂度分析
-
时间复杂度:
- 预处理:DFS 遍历整棵树计算深度、倍增表和最大值数组。时间为 (因为要填充
up数组)。 - 查询:共有 次询问,每次询问有 个人。
- 求 个人的总 LCA 需要做 次 LCA 运算。
- 单次 LCA 运算为 。
- 单次查询总时间:。
- 总时间:。
- 代入数据:,最坏计算量级在 左右,C++ 1秒限时通常可处理 次运算,因此可以稳过。
- 预处理:DFS 遍历整棵树计算深度、倍增表和最大值数组。时间为 (因为要填充
-
空间复杂度:
- 邻接表:。
- 倍增数组
up[N][20]:。 - 其他辅助数组:。
- 总空间:。 字节 ,远低于通常的 128MB/256MB 限制。
标准代码 (C++14)
/** * 题目:P10113 [GESP202312 八级] 大量的工作沟通 * 算法:倍增法求LCA (Lowest Common Ancestor) + 树上前缀最大值预处理 * 难度:提高+/省选- */ #include <iostream> #include <vector> #include <algorithm> #include <cmath> using namespace std; // 定义常量 const int MAXN = 100005; // 最大节点数 const int LOGN = 20; // 倍增层数,2^19 > 100000 // 邻接表存储树结构 vector<int> adj[MAXN]; // up[u][i] 表示节点 u 向上跳 2^i 步到达的祖先 int up[MAXN][LOGN]; // depth[u] 表示节点 u 的深度 int depth[MAXN]; // max_id[u] 表示从根节点到 u 的路径上(包括 u 和 根)编号最大的节点 ID int max_id[MAXN]; int n, q; /** * DFS 预处理 * 功能: * 1. 计算每个节点的深度 depth * 2. 初始化倍增表的第一层 up[u][0] (即父节点) * 3. 预处理路径最大编号 max_id * * @param u 当前节点 * @param p 父节点 * @param d 当前深度 */ void dfs(int u, int p, int d) { depth[u] = d; up[u][0] = p; // 2^0 = 1步,即父节点 // 关键点1:预处理路径上的最大编号 // 递推式:当前路径最大值 = max(当前节点编号, 父节点路径最大值) // 根节点 p=-1,需要特判 if (p != -1) { max_id[u] = max(u, max_id[p]); } else { max_id[u] = u; // 根节点就是自己 } // 遍历子节点 for (int v : adj[u]) { if (v != p) { dfs(v, u, d + 1); } } } /** * 初始化倍增表 * 利用动态规划思想:u 往上跳 2^i 步 = u 先跳 2^(i-1) 步,再从那里跳 2^(i-1) 步 */ void init_lca() { // 注意:0号是根,其父节点设为0或者处理为特殊标记,这里 dfs 中根的父节点传了 -1 // 若 up[u][i-1] 为 -1,说明跳出去了 // 根节点 0 的 up[0][0] 在 dfs 里被设为 -1,为了方便倍增计算,我们可以将根的祖先设为根自己 // 或者在循环中做判断。这里采用将根的父节点设为根自己来避免边界判断 up[0][0] = 0; for (int j = 1; j < LOGN; j++) { for (int i = 0; i < n; i++) { // 状态转移方程 up[i][j] = up[up[i][j - 1]][j - 1]; } } } /** * 倍增法查询两点的 LCA */ int get_lca(int u, int v) { // 1. 保证 u 比 v 深 (或同深) if (depth[u] < depth[v]) swap(u, v); // 2. 将 u 向上跳,直到和 v 同深度 for (int j = LOGN - 1; j >= 0; j--) { // 如果跳完之后深度依然 >= v 的深度,就跳 if (depth[up[u][j]] >= depth[v]) { u = up[u][j]; } } // 3. 如果此时重合,v 就是 LCA if (u == v) return u; // 4. 两个点同时向上跳,直到父亲相同 for (int j = LOGN - 1; j >= 0; j--) { if (up[u][j] != up[v][j]) { u = up[u][j]; v = up[v][j]; } } // 此时 u 和 v 的父节点就是 LCA return up[u][0]; } void solve() { // IO 优化 if (!(cin >> n)) return; // 清空邻接表 (多测习惯,本题单测可略) for (int i = 0; i < n; i++) adj[i].clear(); // 读入 N-1 个父节点信息 // 易错点2:输入格式是 f_1, f_2 ... f_{N-1} // 表示 1 的父亲, 2 的父亲 ... for (int i = 1; i < n; i++) { int f; cin >> f; adj[f].push_back(i); // f 是 i 的父亲,建边 f -> i // 只需要存向下的边即可遍历,因为 dfs 传参带了 parent } // 预处理 // 根节点是 0,父节点设为 -1,深度为 1 (或0) dfs(0, 0, 1); // 修正根节点的父节点指向自己,方便倍增 // 注意:DFS中 up[0][0] 实际上会被赋值为 0 (我们在 dfs 调用时把 p 传 0 即可,或者手动修) // 这里 dfs(0, 0, 1) 表示 0 的父亲是 0,这样跳出根后还是根,不会越界 init_lca(); cin >> q; while (q--) { int m; cin >> m; // 读取第一个人 int curr_lca; cin >> curr_lca; // 关键点3:多人的 LCA 等于累积求 LCA // LCA({a,b,c}) = LCA(LCA(a,b), c) for (int i = 1; i < m; i++) { int p; cin >> p; curr_lca = get_lca(curr_lca, p); } // 关键点4:不要直接输出 LCA! // 题目要求:能够管理所有人的员工中,【编号最大】的那位。 // 我们已经预处理了 max_id 数组,直接查表即可,O(1) cout << max_id[curr_lca] << "\n"; } } int main() { // 开启 IO 优化,处理 10^5 数据量 ios::sync_with_stdio(false); cin.tie(0); solve(); return 0; }代码易错点注释
- 输入下标处理:
- 题目给出的 个数依次对应节点 的父亲。循环要从 开始。
- LCA 基准处理:
- 在
dfs和init_lca中,要注意根节点的父节点处理。如果设为-1,在倍增数组up中需要额外判断边界。一种技巧是将根节点的父亲设为根节点自己(up[0][0] = 0),这样不管怎么倍增向上跳,都会停留在根节点,不会数组越界。
- 在
- HACK 数据应对:
- 很多同学只求了 LCA 就输出了。
- Hack 数据示例:。
- 查询节点 的管理者。LCA 是 。但路径 上,编号最大的是 。
- 如果不使用
max_id数组进行 查询,而是从 LCA 暴力往上爬去找最大值,在链状树的情况下会退化成 ,导致超时。
优化建议
- 快读 (Fast IO):本题数据量较大,
cin必须配合ios::sync_with_stdio(false),否则可能被卡常。 - ST 表 (Sparse Table):求 LCA 也可以转化为 欧拉序列 + ST 表(RMQ 问题),这样预处理 ,查询 LCA 可以达到 。虽然在本题中倍增的 查询已经足够,但 ST 表是更优的解法。由于本题还需要查询路径最大值,倍增体系更加自然(容易顺便维护路径信息)。
-
-
0
你好!我是你的OI教练。很高兴看到你已经从六级题目进阶到了八级题目!
这道题(P10113)其实是六级题目(P10109)的威力加强版。 六级的时候 只有 300,我们可以暴力往上跳。但现在 变成了 ,如果树退化成一条链,暴力跳的时间复杂度会达到 ,直接超时(TLE)。
所以,这道题考察的核心是如何快速处理树上的查询。来,拿出草稿纸,我们一步步拆解。
1. 读题关键词:你的“雷达”响了吗?
请圈出以下决定解题方向的关键信息:
- “直接领导 ”:
- 确认为树形结构,0号是根。
- “ 可以管理 ... 间接领导”:
- 确认为祖先关系。
- “主持合作...管理所有参与同事”:
- 主持人必须是所有参与者的公共祖先。
- 在树上,能够管理集合 的人,一定是这就群人的 最近公共祖先 (LCA) 以及 LCA 的所有祖先(直到根节点)。
- “编号最大的员工”:
- 我们需要在“LCA 到 根节点”这条路径上,找到一个编号最大的节点。
- “”:
- 红色警报:不能再用 的暴力法找 LCA 或遍历路径了。
- 必须使用 或 的算法。
2. 预备知识点
要解决这道题,你需要掌握八级/提高组的核心算法:
- 树的存储:邻接表 (
vector<int> adj[])。 - 最近公共祖先 (LCA):倍增法 (Binary Lifting) 是必备技能。你需要理解
up[u][i]表示 向上跳 步到达的节点。 - 树上预处理 (Tree DP / Prefix Max):如何在 时间内回答“从根到某点路径上的最大值”。
3. 启发式教学:草稿纸上的推演
第一步:理清“主持人”是谁
假设参与者是节点 4 和 5。 在草稿纸上画一棵树:
0 (ID:0) | 1 (ID:1) / \ 2 3 (ID:3) / \ 4 5- 4 的祖先:4, 2, 1, 0
- 5 的祖先:5, 2, 1, 0
- 公共祖先:2, 1, 0
- 最近公共祖先 (LCA):2
所有合法的“主持人”就是 LCA(4, 5) 以及它的所有祖先。也就是路径 上的所有点。
第二步:处理多人的 LCA
如果有 个人 ,怎么找他们的总 LCA?
- 就像求多个数的最大公约数一样:$\text{gcd}(a, b, c) = \text{gcd}(\text{gcd}(a, b), c)$。
- 多人的 LCA 也是迭代的:
- 先求
- 再求
- ...
- 最终的 就是这群人的最低管理层级。
第三步:由慢到快的优化(核心!)
挑战 1:如何快速求 LCA?
- 暴力法:两个点一起一步步往上爬。最坏 。在 数据下太慢。
- 优化法:倍增法 (Doubling)。
- 预处理
up[u][i]。 - 求 LCA 的时间复杂度降为 。
- 思考: 个人求总 LCA,耗时 。可以接受。
- 预处理
挑战 2:如何快速找路径上的最大编号?
- 算出总 LCA 是节点 后,题目要求输出 (根)这条路径上编号最大的点。
- 暴力法:从 一步步往上跳到 0,边跳边打擂台。最坏 。
- 优化法:预处理 (前缀最大值思想)。
- 观察发现:我们总是求“从某点到根”的最大值。这不就是一维数组的“前缀最大值”在树上的变体吗?
- 定义
max_val[u]:从根节点 0 到节点 路径上的最大编号。 - 递推公式:
max_val[u] = max(u的编号, max_val[u的父亲])。 - 这个数组可以在一遍 DFS/BFS 预处理时顺手算出来,时间 。
- 查询:算出 LCA 为 后,直接输出
max_val[L]即可!时间 。
4. 复杂度分析与代码逻辑
我们将思路整理为代码逻辑,并在草稿纸上计算复杂度。
逻辑流程:
- 建树:读取 和父子关系。
- 预处理 (BFS/DFS):
- 计算每个节点的
depth(深度)。 - 计算倍增数组
up[u][i]。 - 关键:计算
max_val[u]。 - 复杂度:。
- 计算每个节点的
- 处理询问:
- 对于每组 个人:
- 令 。
- 循环 :。
- 输出
max_val[L]。 - 复杂度:。
- 对于每组 个人:
算算能不能过:
- ,。
- 预处理: 次运算。轻松!
- 询问:总人数 未知,但题目限制 ,。
- 最坏情况总操作数 $\approx Q \times m \times 17 = 100 \times 10^4 \times 17 \approx 1.7 \times 10^7$。
- C++ 一秒大概能跑 次运算。
- 结论:稳过!
5. 总结关键词
做这类题,你的脑海里要有这些映射:
- 树上找公共祖先 LCA 算法。
- 大数据范围 () 倍增法 ()。
- 根到某点的路径统计 树上 DFS/BFS 预处理 (类似前缀和)。
快去尝试把倍增 LCA 的模板写出来,然后加上
max_val数组的预处理吧!这是通往金牌的必经之路。 - “直接领导 ”:
- 1
信息
- ID
- 14434
- 时间
- 1000ms
- 内存
- 32MiB
- 难度
- 5
- 标签
- 递交数
- 1
- 已通过
- 1
- 上传者