2 条题解
-
0
作为教练,我为你准备了一个针对 “二叉树最大深度” 题目的全自动化数据生成器。
该生成器采用 C++14 标准,能够生成 10 组涵盖从“空树”到“极端链状树”的测试用例。为了保证在 OJ 评测中能区分算法(特别是递归深度限制),我特意加入了 的极端退化树。
数据生成器设计逻辑
- 分级测试点:
- 1-2 (基础边界):空树 () 和 单节点 ()。
- 3-4 (典型结构):小规模随机树与完全二叉树(高度 )。
- 5-7 (极端退化):左斜链、右斜链、之字形链。这些是为了测试选手的程序是否会因为递归过深导致栈溢出(RE)。
- 8-10 (大规模随机): 的随机二叉树。
- 安全性:生成器内置的
solve_depth采用 迭代 BFS 算法,确保在生成.out答案时绝对不会爆栈。 - 合规性:输出格式严格遵循:首行 ,随后 行
val L R,空位用-1表示。
C++ 代码实现
#include <iostream> #include <vector> #include <fstream> #include <queue> #include <algorithm> #include <random> #include <string> using namespace std; // 节点结构 struct Node { int val; int left, right; }; // --------------------------------------------------------- // 核心:使用迭代 BFS 计算深度,防止生成器自身爆栈 // --------------------------------------------------------- int solve_depth(int n, const vector<Node>& tree) { if (n <= 0) return 0; queue<pair<int, int>> q; // <节点编号, 深度> q.push({0, 1}); int max_d = 0; while (!q.empty()) { pair<int, int> curr = q.front(); q.pop(); int u = curr.first; int d = curr.second; max_d = max(max_d, d); if (tree[u].left != -1) q.push({tree[u].left, d + 1}); if (tree[u].right != -1) q.push({tree[u].right, d + 1}); } return max_d; } // --------------------------------------------------------- // 树构造函数 // --------------------------------------------------------- void make_test_case(int id, int n, int type) { string in_name = to_string(id) + ".in"; string out_name = to_string(id) + ".out"; ofstream fin(in_name); ofstream fout(out_name); if (n == 0) { fin << 0 << endl; fout << 0 << endl; return; } vector<Node> tree(n, {0, -1, -1}); mt19937 rng(id + 2025); uniform_int_distribution<int> val_dist(-100, 100); // 填充节点值 for (int i = 0; i < n; ++i) tree[i].val = val_dist(rng); if (type == 1) { // 极端链状:左斜 for (int i = 0; i < n - 1; ++i) tree[i].left = i + 1; } else if (type == 2) { // 极端链状:右斜 for (int i = 0; i < n - 1; ++i) tree[i].right = i + 1; } else if (type == 3) { // 极端链状:之字形 for (int i = 0; i < n - 1; ++i) { if (i % 2 == 0) tree[i].left = i + 1; else tree[i].right = i + 1; } } else if (type == 4) { // 完全二叉树/满二叉树结构 for (int i = 0; i < n; ++i) { int l = 2 * i + 1; int r = 2 * i + 2; if (l < n) tree[i].left = l; if (r < n) tree[i].right = r; } } else { // 随机二叉树 // 保证连通:每个节点 i (i>0) 的父节点 p < i for (int i = 1; i < n; ++i) { int p = uniform_int_distribution<int>(0, i - 1)(rng); // 尝试挂载,如果满位了就往后顺延寻找空位 while (tree[p].left != -1 && tree[p].right != -1) { p = (p + 1) % i; } if (tree[p].left == -1 && tree[p].right == -1) { if (rng() % 2) tree[p].left = i; else tree[p].right = i; } else if (tree[p].left == -1) { tree[p].left = i; } else { tree[p].right = i; } } } // 写入输入文件 fin << n << "\n"; for (int i = 0; i < n; ++i) { fin << tree[i].val << " " << tree[i].left << " " << tree[i].right << "\n"; } // 写入输出文件 fout << solve_depth(n, tree) << endl; fin.close(); fout.close(); printf("Test Case %d [N=%d, Type=%d] Generated.\n", id, n, type); } int main() { // 1-2 边界 make_test_case(1, 0, 0); make_test_case(2, 1, 0); // 3-4 小规模与满二叉树 make_test_case(3, 10, 0); make_test_case(4, 1023, 4); // 深度为 10 的满二叉树 // 5-7 极端链状 (测试递归深度,N=10000) make_test_case(5, 10000, 1); // 左斜 make_test_case(6, 10000, 2); // 右斜 make_test_case(7, 10000, 3); // 之字 // 8-10 大规模随机 make_test_case(8, 5000, 0); make_test_case(9, 10000, 0); make_test_case(10, 10000, 0); return 0; }
给教练(OJ管理员)的提示:
- 文件大小优化:
- 该生成器产生的
.in文件对于 的规模,大小约为 。 - 10 个测试点总大小约 ,非常适合在校园网或小型 OJ 上传输。
- 该生成器产生的
- 区分度分析:
- 测试点 5、6、7 是本题的“灵魂”。如果选手使用递归 DFS 且没有在 Linux 评测环境下设置
ulimit -s unlimited(或者 OJ 没有调整系统栈限制),这些点会导致 Runtime Error。 - 这可以引导学生思考:如何用迭代(BFS 或 模拟栈)去改写递归,这是从 OI 入门级跨越到进阶级的标志。
- 测试点 5、6、7 是本题的“灵魂”。如果选手使用递归 DFS 且没有在 Linux 评测环境下设置
- 时间复杂度说明:
- 生成逻辑中使用了简单的顺延寻找父节点策略,保证了构树是 级别的,生成全部 10 组数据通常在 1 秒内完成。
- 无异常保障:
- 代码中没有除法运算,避免了
Floating Point Exception。 - 对 进行了显式特判,避免了访问
tree[0]的越界错误。
- 代码中没有除法运算,避免了
你可以直接运行此程序,它会在你的当前工作目录下生成
1.in到10.out。祝你的竞赛辅导顺利! - 分级测试点:
-
0
作为教练,我将带你从最基础的分治递归开始,逐步过渡到迭代 BFS,最后讨论针对大规模数据的性能优化写法。这道题虽然简单,但它是理解“树形结构递归收缩”逻辑的敲门砖。
版本一:分治递归 DFS(NOI 考场推荐写法)
思路分析: 这是一棵树,它的深度等于“它最高的子树深度 + 1”。
- 递归出口:如果当前节点是空(编号为 -1),则深度为 0。
- 递推逻辑:
max_depth(u) = max(max_depth(left), max_depth(right)) + 1。 - 时间复杂度:,每个节点遍历一次。
- 空间复杂度:,系统栈空间取决于树的高度。
#include <iostream> #include <algorithm> using namespace std; const int MAXN = 10005; // 定义树节点结构 struct Node { int val; int left, right; } tree[MAXN]; // 递归函数 int get_depth(int u) { // 【关键点】递归终止条件:如果节点为空 (-1),返回深度 0 if (u == -1) { return 0; } // 【易错点】分治思想:先求左子树深度,再求右子树深度 int left_h = get_depth(tree[u].left); int right_h = get_depth(tree[u].right); // 返回较大值 + 1(1代表当前节点这一层) return max(left_h, right_h) + 1; } int main() { // 基础优化:解除 cin 同步,提高读入速度 ios::sync_with_stdio(false); cin.tie(0); int n; if (!(cin >> n) || n == 0) { cout << 0 << endl; return 0; } // 读入节点信息 for (int i = 0; i < n; ++i) { // 在本题中,假设读入顺序即为编号 i cin >> tree[i].val >> tree[i].left >> tree[i].right; } // 根节点固定为 0 cout << get_depth(0) << endl; return 0; }
版本二:广度优先迭代版(稳健的层序遍历)
思路分析: 在某些题目中,树的深度可能非常大(退化成链),递归可能导致系统栈溢出(RE)。使用
std::queue进行层序遍历可以有效规避这个问题。- 核心逻辑:每处理完一层,计数器
depth增加 1。 - 时间复杂度:。
- 空间复杂度:, 是树的最大宽度。
#include <iostream> #include <queue> using namespace std; const int MAXN = 10005; struct Node { int left, right; } tree[MAXN]; int bfs_depth(int n) { if (n == 0) return 0; queue<int> q; q.push(0); // 根节点入队 int depth = 0; while (!q.empty()) { int sz = q.size(); // 【关键】记录当前层有多少个节点 depth++; // 只要这层有东西,深度就加 1 // 处理当前层的所有节点 for (int i = 0; i < sz; ++i) { int u = q.front(); q.pop(); // 将下一层的孩子放入队列 if (tree[u].left != -1) q.push(tree[u].left); if (tree[u].right != -1) q.push(tree[u].right); } } return depth; } int main() { int n; if (!(cin >> n)) return 0; for (int i = 0; i < n; ++i) { int v; cin >> v >> tree[i].left >> tree[i].right; } cout << bfs_depth(n) << endl; return 0; }
版本三:最优性能版(手动队列与 Fast I/O)
思路分析: 在 NOI 竞赛中,如果 且时限很紧,我们会使用
scanf或手写read()函数,并使用数组模拟队列。- 优化点:避免
std::queue的动态内存分配,使用静态数组模拟。
#include <cstdio> #include <algorithm> using namespace std; const int MAXN = 10005; int L[MAXN], R[MAXN]; int que[MAXN]; // 手动模拟队列 int main() { int n; if (scanf("%d", &n) != 1 || n <= 0) { if (n == 0) printf("0\n"); return 0; } for (int i = 0; i < n; ++i) { int v; scanf("%d %d %d", &v, &L[i], &R[i]); } int head = 0, tail = 0; que[tail++] = 0; // 根节点 int depth = 0; while (head < tail) { int sz = tail - head; // 当前层的节点数 depth++; for (int i = 0; i < sz; ++i) { int u = que[head++]; // 压入子节点到队列尾部 if (L[u] != -1) que[tail++] = L[u]; if (R[u] != -1) que[tail++] = R[u]; } } printf("%d\n", depth); return 0; }
复杂度分析思考过程
-
时间复杂度分析:
- DFS 版本:每个节点作为递归函数的参数进入一次,离开一次。。
- BFS 版本:每个节点入队一次,出队一次。。
- 结论:对于树的遍历问题, 是理论下限。
-
空间复杂度分析:
- 递归空间:取决于递归深度。对于平衡二叉树,;对于退化成链的二叉树,。因此最坏情况空间复杂度是 。
- 队列空间:取决于树的最大宽度。完全二叉树最后一层大约有 个节点,所以也是 。
-
时间复杂度优化建议:
- 预分配空间:在 NOI 竞赛中,尽量使用全局静态数组而非
std::vector或动态new节点。 - I/O 瓶颈:当 时,
cin即使关闭同步也可能比scanf慢。此时应考虑使用fread快读。
- 预分配空间:在 NOI 竞赛中,尽量使用全局静态数组而非
总结:读题关键词在哪里?
- “最大深度”:
- 看到这个词,联想递归公式:
f(u) = max(f(L), f(R)) + 1。
- 看到这个词,联想递归公式:
- “节点总数 ”:
- 这个范围非常小。在 NOI 中,这意味着你可以随意使用递归、STL,甚至一些常数略大的算法。但如果范围是 ,则必须考虑系统栈空间。
- “-1 表示不存在”:
- 这是建图的关键。在递归和迭代中,都要先判断这个值。
- “根节点固定为 0”:
- 简化了题意,不需要你去寻找入度为 0 的节点作为根。
教练提示:在考场上,首选递归法,因为它代码量最少,逻辑最清晰。只有当你观察到数据范围巨大且可能存在链状结构时,才考虑切换到迭代 BFS。加油!
- 1
信息
- ID
- 19414
- 时间
- 1000ms
- 内存
- 128MiB
- 难度
- 10
- 标签
- (无)
- 递交数
- 1
- 已通过
- 1
- 上传者