2 条题解
-
0
作为教练,我为你准备了一个全自动的数据生成器。它能生成 10 组测试数据,涵盖了边界情况()、特殊形态(链状、满二叉树)以及大规模随机树。
为了确保生成器本身的稳健性,生成
.out答案的代码采用了 复杂度的非递归/递归算法(在 规模下递归是安全的),并对节点值进行了打乱处理以保证唯一性。C++ 数据生成器 (gen.cpp)
#include <iostream> #include <vector> #include <string> #include <fstream> #include <algorithm> #include <random> #include <stack> #include <map> using namespace std; // --- 树的结构定义 --- struct Node { int val; int l, r; Node() : val(0), l(-1), r(-1) {} }; // --- 全局变量 --- vector<Node> tree_pool; vector<int> in_seq, post_seq, pre_seq; // --- 辅助函数:获取各种遍历序列 --- void get_traversals(int u) { if (u == -1) return; // 中序: 左 根 右 get_traversals(tree_pool[u].l); in_seq.push_back(tree_pool[u].val); // 后序: 左 右 根 get_traversals(tree_pool[u].r); post_seq.push_back(tree_pool[u].val); } // 获取答案(前序序列) void get_preorder(int u) { if (u == -1) return; pre_seq.push_back(tree_pool[u].val); get_preorder(tree_pool[u].l); get_preorder(tree_pool[u].r); } // --- 数据生成逻辑 --- void make_tree(int n, int type, int seed) { tree_pool.assign(n, Node()); mt19937 rng(seed); // 生成不重复的节点值 [-3000, 3000] vector<int> vals; for (int i = -3000; i <= 3000; ++i) vals.push_back(i); shuffle(vals.begin(), vals.end(), rng); for (int i = 0; i < n; ++i) tree_pool[i].val = vals[i]; if (type == 1) { // 左斜链 for (int i = 0; i < n - 1; ++i) tree_pool[i].l = i + 1; } else if (type == 2) { // 右斜链 for (int i = 0; i < n - 1; ++i) tree_pool[i].r = i + 1; } else if (type == 3) { // 满二叉树结构 for (int i = 0; i < n; ++i) { int left = 2 * i + 1; int right = 2 * i + 2; if (left < n) tree_pool[i].l = left; if (right < n) tree_pool[i].r = right; } } else { // 随机树 for (int i = 1; i < n; ++i) { int p = uniform_int_distribution<int>(0, i - 1)(rng); // 确保每个节点最多两个孩子 while (tree_pool[p].l != -1 && tree_pool[p].r != -1) { p = (p + 1) % i; } if (tree_pool[p].l == -1) tree_pool[p].l = i; else tree_pool[p].r = i; } } } // --- 主生成流程 --- void generate(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); // 1. 生成树结构 make_tree(n, type, id + 100); // 2. 获取中序和后序 (写入 .in) in_seq.clear(); post_seq.clear(); get_traversals(0); fin << n << "\n"; for (int i = 0; i < n; ++i) fin << in_seq[i] << (i == n - 1 ? "" : " "); fin << "\n"; for (int i = 0; i < n; ++i) fin << post_seq[i] << (i == n - 1 ? "" : " "); fin << "\n"; // 3. 获取前序 (写入 .out 作为标准答案) pre_seq.clear(); get_preorder(0); for (int i = 0; i < n; ++i) fout << pre_seq[i] << (i == n - 1 ? "" : " "); fout << "\n"; fin.close(); fout.close(); cout << "Test Case " << id << " [n=" << n << "] finished." << endl; } int main() { // 1. 边界情况 generate(1, 1, 0); generate(2, 5, 0); // 2. 满二叉树 (规模适中) generate(3, 1023, 3); // 3. 极端链状 (测试递归深度和逻辑边界) generate(4, 3000, 1); // 左链 generate(5, 3000, 2); // 右链 // 4. 大规模随机树 for (int i = 6; i <= 10; ++i) { generate(i, 3000, 0); } return 0; }
数据说明及优化分析
-
文件大小控制:
- 对于 的数据,每一行大约包含 3000 个整数。假设每个整数平均 5 字节(包含空格),则一行约 。
- 每个测试点(
.in+.out)总计约 。 - 10 组测试点总大小约为 450KB,远低于 2MB 的理想值,非常适合 OJ 评测。
-
生成效率:
- 树构造:采用随机分配父节点的方法,复杂度 或在节点满载时略有退化,但在 下几乎瞬时完成。
- 序列生成:采用简单的递归 DFS。由于 ,系统栈(通常 8MB)足以处理 3000 层的递归深度,不会爆栈。
- 无除法风险:代码中仅在随机数分布中使用除法(由库函数处理),构树逻辑中不涉及除以 0 的风险。
-
测试点针对性设计:
- Case 4 & 5:极端斜树。这对于那些在分割区间时计算错误的程序非常有效,能够准确捕捉到索引加减 1 的错误。
- Case 3:满二叉树。测试程序在处理平衡结构时的表现。
- Case 6-10:通过
mt19937随机引擎生成,节点值在 范围内随机选取且保证不重复,覆盖了复杂的一般情况。
教练提示:
- OJ 判题建议:本题输出为一行整数。判题时应建议开启“忽略行末空格”和“忽略空行”,以兼容不同选手的输出习惯。
- 选手注意:由于 达到了 3000,虽然不一定要哈希优化,但建议选手使用
std::map或静态数组做索引映射,以确保 的稳定表现。
你可以直接编译并运行该程序,它会在当前目录下生成
1.in到10.out。祝辅导顺利! -
-
0
作为教练,我将带你从暴力搜索版过渡到哈希映射版。
在重构二叉树时,核心公式是:子树的节点数量(Size)在所有遍历序列中都是守恒的。
版本一:基础递归版(线性搜索 )
思路分析: 这是最符合直觉的写法。我们通过后序序列的末尾确定根节点,然后在中间序列中通过
for循环寻找该根节点。- 时间复杂度:。寻找中序索引需 ,总共 个节点。
- 空间复杂度:。用于存储树结构和递归栈。
- 评价:对于 ,运行次数约为 ,在 NOIP 1.0s 的时限内绰绰有余。
#include <iostream> #include <vector> using namespace std; const int MAXN = 3005; struct Node { int val, l, r; } tree[MAXN]; int inorder[MAXN], postorder[MAXN]; int node_cnt = 0; // 静态分配节点编号 // inL, inR: 当前子树在中序序列的区间范围 // postL, postR: 当前子树在后序序列的区间范围 int build(int inL, int inR, int postL, int postR) { if (inL > inR || postL > postR) return -1; // 递归边界 // 1. 后序遍历的最后一个元素就是根 int root_val = postorder[postR]; int u = node_cnt++; tree[u].val = root_val; // 2. 在中序遍历中通过线性扫描找到根的位置 k int k = inL; while (k <= inR && inorder[k] != root_val) { k++; } // 3. 计算左子树的大小 int left_size = k - inL; // 4. 递归构建左右子树 // 【关键】左子树:后序区间从 postL 开始,长度为 left_size tree[u].l = build(inL, k - 1, postL, postL + left_size - 1); // 【关键】右子树:后序区间紧跟左子树之后,排除掉最后的根节点 tree[u].r = build(k + 1, inR, postL + left_size, postR - 1); return u; } // 验证用:前序遍历输出 void preOrder(int u, int n) { static int cnt = 0; if (u == -1) return; cout << tree[u].val << (++cnt == n ? "" : " "); preOrder(tree[u].l, n); preOrder(tree[u].r, n); } int main() { int n; if (!(cin >> n)) return 0; for (int i = 0; i < n; i++) cin >> inorder[i]; for (int i = 0; i < n; i++) cin >> postorder[i]; int root_id = build(0, n - 1, 0, n - 1); preOrder(root_id, n); cout << endl; return 0; }
版本二:哈希优化版(竞赛标准 )
思路分析: 为了将复杂度降到 ,我们需要消除递归内部的
while循环。由于节点值唯一且范围已知,我们可以使用一个数组pos作为简易哈希表,预存每个值在中序序列中的下标。- 时间复杂度:。预处理 ,递归每个节点 。
- 空间复杂度:。
- 易错点:节点值有负数,数组下标需加
OFFSET。
#include <iostream> using namespace std; const int MAXN = 3005; const int OFFSET = 3000; // 处理负数索引 struct Node { int val, l, r; } tree[MAXN]; int inorder[MAXN], postorder[MAXN]; int pos[6005]; // pos[val + OFFSET] 存储该值在中序中的下标 int node_cnt = 0; int build(int inL, int inR, int postL, int postR) { if (inL > inR) return -1; int root_val = postorder[postR]; int u = node_cnt++; tree[u].val = root_val; // 【优化】通过预处理数组直接获取中序下标,无需循环 int k = pos[root_val + OFFSET]; int left_size = k - inL; tree[u].l = build(inL, k - 1, postL, postL + left_size - 1); tree[u].r = build(k + 1, inR, postL + left_size, postR - 1); return u; } void preOrder(int u, int n) { static int cnt = 0; if (u == -1) return; cout << tree[u].val << (++cnt == n ? "" : " "); preOrder(tree[u].l, n); preOrder(tree[u].r, n); } int main() { // NOI 技巧:关闭 cin 同步提升速度 ios::sync_with_stdio(false); cin.tie(0); int n; if (!(cin >> n)) return 0; for (int i = 0; i < n; i++) { cin >> inorder[i]; pos[inorder[i] + OFFSET] = i; // 预处理 } for (int i = 0; i < n; i++) cin >> postorder[i]; int root_id = build(0, n - 1, 0, n - 1); preOrder(root_id, n); cout << endl; return 0; }
复杂度分析思考过程
-
时间复杂度思考:
- 寻找根位置:这是本题的瓶颈。如果每次都扫描中序数组,最坏情况(树退化成链)会导致 。通过哈希表(或数组映射)将其变为 ,是树论题目的通用优化手段。
- 遍历所有节点:递归函数
build会被调用 次(包括空节点返回),整体线性。
-
空间复杂度思考:
- 静态存储:在 NOIP 中,使用
Node tree[MAXN]比new Node()动态内存分配快得多,且不容易产生内存碎片。 - 栈空间:递归深度 。 即使是链状树( 层递归),系统默认的栈空间(通常 8MB)也足够。如果 达到 ,则需要考虑手工模拟栈。
- 静态存储:在 NOIP 中,使用
-
时间复杂度优化建议:
- 内联函数:对于频繁调用的简单操作可以加
inline。 - 快读:如果数据量极大,使用
getchar()实现的快速读入。
- 内联函数:对于频繁调用的简单操作可以加
总结:读题关键词与易错点
- “无重复元素”:这是此类题目能唯一确定二叉树的前提。
- “后序”:一定要死死盯住
postR,它是当前子树的“灵魂”。 - “左右子树规模”:这是切分数组的依据。记住
left_size = k - inL,那么后序的左子树区间就是[postL, postL + left_size - 1]。 - “-1 边界”:在静态存储中,
-1代表空指针。初始化和判断必须严谨。
教练提示:在考场上,区间下标的计算最容易出错。建议你在草稿纸上画一段长度为 3 的左子树例子,手动代入
inL, k, postL算一次,确保+1和-1没有写反。加油!
- 1
信息
- ID
- 19420
- 时间
- 1000ms
- 内存
- 128MiB
- 难度
- 10
- 标签
- (无)
- 递交数
- 1
- 已通过
- 1
- 上传者