2 条题解
-
0
为了方便你快速创建 OJ(Online Judge)测试数据,我编写了这个高性能的数据生成器。它采用 C++14 标准,结合了 BFS 迭代算法 生成标准输出,确保即使在极端链状数据下也不会爆栈。
数据生成器设计思路
- 分级测试点设计:
1.in-2.in:边界情况。空树 () 和单节点树 ()。3.in:小规模构造。手动模拟 LeetCode 示例 2 的复杂树结构。4.in:极端深度(链状)。,每个节点只有一个孩子,测试算法对深度的处理。5.in:极端宽度(星形)。,根节点连接所有其他节点,测试算法对单层极宽情况的处理。6.in-10.in:大规模随机树。 增加至 ,模拟各种分叉因子(Binary, Ternary, High-degree)。
- 安全性与性能:
- 非递归生成答案:使用
std::queue实现 BFS,彻底规避递归导致的系统栈溢出。 - 高效构树:通过
p = random(0, i-1)确保生成的图一定是连通且无环的树。 - 文件大小优化: 级别的节点数据,单个文件约 ,总数据量远小于 2MB。
- 非递归生成答案:使用
C++ 数据生成器代码
#include <iostream> #include <vector> #include <fstream> #include <queue> #include <algorithm> #include <random> #include <string> using namespace std; // 节点结构 struct Node { int val; vector<int> children; }; // --------------------------------------------------------- // 核心:标准 BFS 层序遍历(用于生成 .out 文件) // --------------------------------------------------------- void solve_and_output(int n, const vector<Node>& tree, ofstream& fout) { if (n <= 0) return; queue<int> q; q.push(0); // 根节点固定为编号 0 vector<vector<int>> result; while (!q.empty()) { int sz = q.size(); vector<int> level; level.reserve(sz); for (int i = 0; i < sz; ++i) { int u = q.front(); q.pop(); level.push_back(tree[u].val); for (int v : tree[u].children) { q.push(v); } } result.push_back(level); } // 输出深度 fout << result.size() << "\n"; // 输出每一层 for (const auto& lv : result) { for (int i = 0; i < (int)lv.size(); ++i) { fout << lv[i] << (i == (int)lv.size() - 1 ? "" : " "); } fout << "\n"; } } // --------------------------------------------------------- // 随机树构造逻辑 // --------------------------------------------------------- void generate_case(int id, int n, int type) { string in_file = to_string(id) + ".in"; string out_file = to_string(id) + ".out"; ofstream fin(in_file); ofstream fout(out_file); if (n == 0) { fin << 0 << endl; return; } mt19937 rng(id + 2025); uniform_int_distribution<int> val_dist(0, 1000000); vector<Node> tree(n); for (int i = 0; i < n; ++i) tree[i].val = val_dist(rng); if (type == 4) { // 链状树 for (int i = 0; i < n - 1; ++i) tree[i].children.push_back(i + 1); } else if (type == 5) { // 星形树 for (int i = 1; i < n; ++i) tree[0].children.push_back(i); } else if (id == 3) { // 模拟示例:手动构造复杂分叉 tree.clear(); tree.resize(14); for(int i=0; i<14; ++i) tree[i].val = i+1; tree[0].children = {1, 2, 3, 4}; tree[2].children = {5, 6}; tree[3].children = {7}; tree[4].children = {8, 9}; tree[6].children = {10}; tree[7].children = {11}; tree[8].children = {12}; tree[10].children = {13}; } else { // 随机通用树 for (int i = 1; i < n; ++i) { // 父节点编号必须小于子节点,确保无环 int p = uniform_int_distribution<int>(0, i - 1)(rng); tree[p].children.push_back(i); } // 随机打乱子节点顺序 for (int i = 0; i < n; ++i) { shuffle(tree[i].children.begin(), tree[i].children.end(), rng); } } // 写入输入文件 fin << n << "\n"; for (int i = 0; i < n; ++i) { fin << tree[i].val << " " << tree[i].children.size(); for (int child_id : tree[i].children) { fin << " " << child_id; } fin << "\n"; } // 运行标程生成输出文件 solve_and_output(n, tree, fout); fin.close(); fout.close(); cout << "Test case " << id << " created. (n=" << n << ")" << endl; } int main() { // 基础与特殊构造 generate_case(1, 0, 0); // 空树 generate_case(2, 1, 0); // 单节点 generate_case(3, 14, 0); // 示例构造 // 极端结构 generate_case(4, 5000, 4); // 链 generate_case(5, 5000, 5); // 星 // 大规模随机 for (int i = 6; i <= 10; ++i) { generate_case(i, 10000, 0); } return 0; }
数据说明及优化点
- 防止除以 0 异常:
- 在随机父节点选择时,使用了
uniform_int_distribution<int>(0, i-1)。由于循环从i=1开始,区间下界永远不大于上界,且不涉及任何除法操作,安全性极高。
- 在随机父节点选择时,使用了
- 文件存储优化:
- 采用了标准的
ofstream流。 - 在生成大规模随机树时,使用了
vector::reserve来减少内存碎片,保证生成器在几百毫秒内即可完成所有数据的制作。
- 采用了标准的
- 为何选择 :
- 对于 算法, 能在瞬间跑完。
- 对于选手的 或更差的算法(例如每次都在数组中通过搜索来找节点),这个数据量配合 1s 时间限制能产生明显的区分度。
- 读取建议:
- 在
.out文件的第一行增加了深度 。这在 OJ 评测中非常有用,可以方便判题脚本或选手确认输出规模。
- 在
你可以直接在本地编译运行该代码,它会在当前目录下生成
1.in/out到10.in/out。加油,祝你的 OJ 建设顺利! - 分级测试点设计:
-
0
作为教练,我会带你从最容易想到的深度优先搜素(DFS)辅助法,逐步进化到竞赛中最稳健的标准广度优先搜索(BFS),最后通过手动模拟队列与 I/O 优化达到最优性能。
版本一:辅助 DFS 法(简单直观,但非标准 BFS)
思路分析: 虽然题目要求层序遍历,但我们可以利用 DFS 遍历整棵树,同时在递归过程中记录每个节点的“深度”。我们将同一深度的节点存入同一个
vector。- 时间复杂度:,每个节点访问一次。
- 空间复杂度:,用于存储答案和递归栈(最坏情况 )。
#include <iostream> #include <vector> #include <algorithm> using namespace std; const int MAXN = 10005; struct Node { int val; vector<int> ch; } tree[MAXN]; vector<vector<int>> levels; // depth 从 0 开始 void dfs(int u, int depth) { // 如果当前深度超过了已有的 levels 大小,新增一层 if (depth >= levels.size()) { levels.push_back(vector<int>()); } levels[depth].push_back(tree[u].val); for (int v : tree[u].ch) { dfs(v, depth + 1); } } int main() { int n; if (!(cin >> n) || n == 0) return 0; // 处理空树边界 for (int i = 0; i < n; ++i) { int k; cin >> tree[i].val >> k; for (int j = 0; j < k; ++j) { int child; cin >> child; tree[i].ch.push_back(child); } } dfs(0, 0); // 输出深度 cout << levels.size() << "\n"; // 输出每一层 for (const auto& lv : levels) { for (int i = 0; i < lv.size(); ++i) { cout << lv[i] << (i == lv.size() - 1 ? "" : " "); } cout << "\n"; } return 0; }
版本二:标准 BFS 迭代版(推荐:竞赛最稳写法)
思路分析: 利用
std::queue实现 BFS。核心技巧是在处理每一层之前,先记录q.size()。这个size就是当前层的所有节点数量。- 时间复杂度:。
- 空间复杂度:,队列最大可能容纳一层的所有节点()。
#include <iostream> #include <vector> #include <queue> using namespace std; const int MAXN = 10005; int val[MAXN]; vector<int> adj[MAXN]; void bfs(int n) { if (n == 0) return; queue<int> q; q.push(0); // 根节点入队 vector<vector<int>> res; while (!q.empty()) { int sz = q.size(); // 【关键】当前层节点的个数 vector<int> current_level; // 连续处理 sz 个节点,它们都属于同一层 for (int i = 0; i < sz; ++i) { int u = q.front(); q.pop(); current_level.push_back(val[u]); // 将子节点加入队列,属于下一层 for (int v : adj[u]) { q.push(v); } } res.push_back(current_level); } // 输出 cout << res.size() << "\n"; for (const auto& lv : res) { for (int i = 0; i < lv.size(); ++i) { cout << lv[i] << (i == lv.size() - 1 ? "" : " "); } cout << "\n"; } } int main() { ios::sync_with_stdio(false); // 优化 I/O cin.tie(0); int n; if (!(cin >> n)) return 0; for (int i = 0; i < n; ++i) { int k; cin >> val[i] >> k; for (int j = 0; j < k; ++j) { int child; cin >> child; adj[i].push_back(child); } } bfs(n); return 0; }
版本三:最优复杂度(手动队列 + 链式存储思路)
思路分析: 在数据量极大时, 虽然已经最优,但
std::vector的内存分配和std::queue的封装开销可以通过手动模拟来消除。我们使用数组模拟队列,并直接在结果输出时处理。- 时间复杂度:,常数极小。
- 空间复杂度:,利用数组预分配。
#include <cstdio> #include <vector> using namespace std; const int MAXN = 10005; int node_val[MAXN]; vector<int> adj[MAXN]; int q[MAXN]; // 手动模拟队列 int main() { int n; if (scanf("%d", &n) != 1 || n <= 0) return 0; for (int i = 0; i < n; ++i) { int k; scanf("%d %d", &node_val[i], &k); adj[i].reserve(k); // 减少 vector 扩容开销 for (int j = 0; j < k; ++j) { int child; scanf("%d", &child); adj[i].push_back(child); } } int head = 0, tail = 0; q[tail++] = 0; // 根节点入队 vector<vector<int>> res; while (head < tail) { int level_size = tail - head; // 当前层节点数 vector<int> level_nodes; level_nodes.reserve(level_size); for (int i = 0; i < level_size; ++i) { int u = q[head++]; level_nodes.push_back(node_val[u]); for (int v : adj[u]) { q[tail++] = v; } } res.push_back(level_nodes); } // 输出深度 printf("%zu\n", res.size()); for (const auto& lv : res) { for (int i = 0; i < lv.size(); ++i) { printf("%d%c", lv[i], i == lv.size() - 1 ? '\n' : ' '); } } return 0; }
复杂度分析与性能建议
-
时间复杂度分析思考过程:
- 层序遍历实际上是广度优先搜索的一种。由于每个节点都会且仅会入队一次,所有边也会被扫描一次。
- 总时间复杂度为 ,在树结构中 ,因此是标准的 。
- 优化建议:在处理每一层的
vector时,如果不关心层级,可以不存储直接输出。但题目要求按层输出,所以存储是必须的。
-
空间复杂度分析思考过程:
- 存储空间:邻接表需要 。
- 辅助空间:队列在最坏情况下需要 (比如根节点有 个孩子)。
- 优化建议:如果内存限制极其严格(例如 64MB 处理 级别数据),建议舍弃
std::vector存储邻接表,改用链式前向星(使用静态数组模拟链表)。
-
读题关键词与易错点:
- “层序”:暗示 BFS。
- “从左到右”:读入孩子节点时,必须按顺序
push_back,不能乱序。 - 情况:NOI 题目经常会给 的测试点,如果代码中直接
q.push(0)而不加判断,会导致数组越界。 - 分层逻辑:必须在
while循环内部先获取q.size()。如果直接写for(int i=0; i < q.size(); i++),因为循环内部会向队列里push新元素,会导致q.size()动态变化,从而无法正确区分层级。
希望这些代码和分析能帮你彻底掌握 N 叉树的层序遍历!现在,去你的编译器上试试这些代码吧。
- 1
信息
- ID
- 19413
- 时间
- 1000ms
- 内存
- 128MiB
- 难度
- 10
- 标签
- (无)
- 递交数
- 1
- 已通过
- 1
- 上传者