2 条题解
-
0
这个错误是因为在之前的代码片段中,我使用了
MAXN作为一个占位符常数,但没有在代码开头显式定义它。作为 NOI 教练,我为您整理了一份完整、可直接编译运行的数据生成器。它不仅修复了
MAXN未定义的问题,还避开了std::generate的命名冲突,并且包含了完整的 BFS 求解逻辑,以确保生成的.out文件绝对准确。1. 完善后的数据生成器 (C++14)
#include <iostream> #include <fstream> #include <vector> #include <queue> #include <algorithm> #include <random> #include <string> #include <set> using namespace std; // 定义全局最大节点数,对应题目范围 n <= 500 const int MAXN = 505; struct Node { int v, l, r; }; // ---------------- 核心算法:求解器用于生成标准答案 (.out) ---------------- void solve_and_write_output(int n, int target, int k, const vector<Node>& nodes, string out_name) { // 1. 建立邻接表(树转图) vector<int> adj[MAXN]; for (const auto& node : nodes) { if (node.l != -1) { adj[node.v].push_back(node.l); adj[node.l].push_back(node.v); } if (node.r != -1) { adj[node.v].push_back(node.r); adj[node.r].push_back(node.v); } } // 2. BFS 搜索距离为 k 的点 queue<pair<int, int>> q; bool vis[MAXN] = {false}; vector<int> result; q.push({target, 0}); vis[target] = true; while (!q.empty()) { pair<int, int> curr = q.front(); q.pop(); if (curr.second == k) { result.push_back(curr.first); continue; } for (int neighbor : adj[curr.first]) { if (!vis[neighbor]) { vis[neighbor] = true; q.push({neighbor, curr.second + 1}); } } } // 3. 排序并写入文件 sort(result.begin(), result.end()); ofstream fout(out_name); for (int i = 0; i < result.size(); i++) { fout << result[i] << (i == result.size() - 1 ? "" : " "); } fout << endl; // 即使结果为空也输出换行,符合 NOI 规范 fout.close(); } // ---------------- 随机工具与生成逻辑 ---------------- mt19937 rng(1337); int randInt(int l, int r) { return uniform_int_distribution<int>(l, r)(rng); } void create_test_point(int t) { string in_name = to_string(t) + ".in"; string out_name = to_string(t) + ".out"; int n, target, k; // 根据测试点编号设定不同规模和场景 if (t == 1) { n = 1; target = 0; k = 0; } // 最小边界 else if (t <= 5) { n = randInt(10, 50); k = randInt(1, 5); } // 小规模 else { n = 500; k = randInt(2, 20); } // 最大规模测试 target = randInt(0, n - 1); if (t == 4) k = 1000; // 测试 k > n 的无解情况 // 生成随机排列的权值 vector<int> vals(n); for(int i = 0; i < n; i++) vals[i] = i; shuffle(vals.begin(), vals.end(), rng); vector<Node> tree_nodes(n); for(int i = 0; i < n; i++) tree_nodes[i] = {vals[i], -1, -1}; // 构建树结构(非递归方式) vector<int> pool; // 记录还有空余孩子的节点索引 pool.push_back(0); vector<int> children_count(n, 0); for (int i = 1; i < n; i++) { int p_idx = randInt(0, pool.size() - 1); int p = pool[p_idx]; if (children_count[p] == 0) { if (randInt(0, 1)) tree_nodes[p].l = tree_nodes[i].v; else tree_nodes[p].r = tree_nodes[i].v; } else { if (tree_nodes[p].l == -1) tree_nodes[p].l = tree_nodes[i].v; else tree_nodes[p].r = tree_nodes[i].v; } children_count[p]++; if (children_count[p] >= 2) { pool.erase(pool.begin() + p_idx); // 孩子满了,移出池子 } pool.push_back(i); // 新节点进入池子 } // 写入 .in 文件 ofstream fin(in_name); fin << n << " " << target << " " << k << endl; for (int i = 0; i < n; i++) { fin << tree_nodes[i].v << " " << tree_nodes[i].l << " " << tree_nodes[i].r << endl; } fin.close(); // 生成 .out 文件 solve_and_write_output(n, target, k, tree_nodes, out_name); cout << "Test point " << t << " [n=" << n << "] finished." << endl; } int main() { for (int i = 1; i <= 10; i++) { create_test_point(i); } return 0; }
2. 预备知识点总结 (针对本题)
作为教练,我要求你在写代码前必须理解这三点:
- 二叉树的双向性:在普通的 DFS 中,我们只能“向下”走。要实现“向上”走,必须在输入时记录每个节点的父节点,或者像上面代码一样转化成无向图邻接表。
- BFS 的层级特性:BFS 是一层一层扩散的,非常适合求解“距离为 K”的问题。
- 防环意识:在图遍历中,如果没有
vis数组,你会在父节点和子节点之间来回跳跃,导致死循环。
3. 读题关键词 (考场避坑)
在 NOI 考场上,看到以下词汇要敏感:
- “距离为 K” 暗示使用 BFS。
- “权值互不相同” 说明可以直接用权值作为数组下标(如
adj[val]),不需要复杂的指针或哈希映射。 - “所有结点” 暗示可能有多个答案,最后记得排序输出(NOI 风格通常要求升序)。
4. 复杂度与优化建议
- 时间复杂度:。建图 ,BFS ,排序 。对于 来说,绰绰有余。
- 空间复杂度:。主要开销在邻接表和
vis数组。 - 优化:如果你觉得
vector<int> adj[MAXN]慢,可以使用 链式前向星(静态数组模拟邻接表),这是金牌选手在处理大数据量时的常用技巧。
现在,你可以直接编译运行这段生成器代码,它将完美产生 10 组测试数据。
-
0
在解决“树中距离为 的节点”问题时,最关键的思维转变是:将“有向”的树结构视作“无向”的图结构。
以下提供三个版本的代码:从最基础的图转化法,到递归维护父节点的 DFS 法,再到针对小规模数据的竞赛优化版。
版本一:树转图 + BFS(最稳健的竞赛写法)
思路: 这是 NOI 选手的首选思路。由于二叉树本质上是一个连通图,我们只需要建立邻接表,把“父子关系”变成“双向边”,然后以
target为起点跑一次广度优先搜索(BFS)即可。#include <iostream> #include <vector> #include <queue> #include <algorithm> using namespace std; const int MAXN = 505; vector<int> adj[MAXN]; // 邻接表存储图 bool visited[MAXN]; // 访问标记,防止 BFS 回流 int n, target, k; int main() { // 1. 读取数据并建图 if (!(cin >> n >> target >> k)) return 0; for (int i = 0; i < n; i++) { int v, l, r; cin >> v >> l >> r; if (l != -1) { adj[v].push_back(l); adj[l].push_back(v); // 建立双向边 } if (r != -1) { adj[v].push_back(r); adj[r].push_back(v); // 建立双向边 } } // 2. 从 target 开始 BFS queue<pair<int, int>> q; // <当前节点, 当前距离> q.push({target, 0}); visited[target] = true; vector<int> res; while (!q.empty()) { pair<int, int> curr = q.front(); q.pop(); int u = curr.first; int dist = curr.second; if (dist == k) { res.push_back(u); continue; // 达到距离 K,不再向外扩展 } for (int v : adj[u]) { if (!visited[v]) { visited[v] = true; q.push({v, dist + 1}); } } } // 3. 排序并输出 sort(res.begin(), res.end()); for (int i = 0; i < res.size(); i++) { cout << res[i] << (i == res.size() - 1 ? "" : " "); } cout << endl; // 若 res 为空,此行输出空行,符合题意 return 0; }
版本二:DFS + 父节点映射(递归版)
思路: 不显式建立邻接表,而是通过一次 DFS 记录每个节点的父节点
parent[child] = father。然后从target出发向三个方向(左、右、上)进行递归搜索。#include <iostream> #include <vector> #include <algorithm> using namespace std; const int MAXN = 505; int L[MAXN], R[MAXN], fa[MAXN]; // 存储左、右孩子和父节点 vector<int> res; bool vis[MAXN]; // d: 当前距离, k: 目标距离, from: 来源节点(防止往回走) void find_k(int u, int d, int k) { if (u == -1 || vis[u]) return; vis[u] = true; if (d == k) { res.push_back(u); return; } find_k(L[u], d + 1, k); // 向左走 find_k(R[u], d + 1, k); // 向右走 find_k(fa[u], d + 1, k); // 向上走 } int main() { int n, target, k; if (!(cin >> n >> target >> k)) return 0; // 初始化父节点 for(int i=0; i<MAXN; ++i) fa[i] = -1; for (int i = 0; i < n; i++) { int v, l, r; cin >> v >> l >> r; L[v] = l; R[v] = r; if (l != -1) fa[l] = v; if (r != -1) fa[r] = v; } find_k(target, 0, k); sort(res.begin(), res.end()); for (int i = 0; i < res.size(); i++) { cout << res[i] << (i == res.size() - 1 ? "" : " "); } cout << endl; return 0; }
版本三:NOI 竞赛级常数优化版(静态邻接表)
思路: 在 较大的情况下,
std::vector的动态开辟和cin会成为瓶颈。本版本使用链式前向星(静态邻接表)和数组模拟队列,将常数压到极致。#include <cstdio> #include <algorithm> using namespace std; const int MAXN = 505; const int MAXE = 2005; // 边数:n个节点,n-1条树边,双向则约2n int head[MAXN], to[MAXE], nxt[MAXE], tot; int q[MAXN], dist[MAXN]; bool vis[MAXN]; // 链式前向星加边 void add_edge(int u, int v) { to[++tot] = v; nxt[tot] = head[u]; head[u] = tot; } int main() { int n, target, k; if (scanf("%d %d %d", &n, &target, &k) == EOF) return 0; for (int i = 0; i < n; i++) { int v, l, r; scanf("%d %d %d", &v, &l, &r); if (l != -1) { add_edge(v, l); add_edge(l, v); } if (r != -1) { add_edge(v, r); add_edge(r, v); } } // 数组模拟 BFS 队列 int l = 0, r = 0; q[r++] = target; vis[target] = true; dist[target] = 0; int ans[MAXN], cnt = 0; while (l < r) { int u = q[l++]; if (dist[u] == k) { ans[cnt++] = u; continue; } for (int i = head[u]; i; i = nxt[i]) { int v = to[i]; if (!vis[v]) { vis[v] = true; dist[v] = dist[u] + 1; q[r++] = v; } } } sort(ans, ans + cnt); for (int i = 0; i < cnt; i++) { printf("%d%c", ans[i], i == cnt - 1 ? '\n' : ' '); } if (cnt == 0) printf("\n"); return 0; }
复杂度分析与优化建议
1. 时间复杂度分析:
- 建图过程:遍历 个节点,每个节点处理 2 条边。复杂度 。
- 搜索过程:无论是 BFS 还是 DFS,由于每个节点最多被访问一次,每个邻接边最多被遍历一次。复杂度 。
- 排序输出:。
- 总体:。在 的量级下,耗时通常小于 1ms。
2. 空间复杂度分析:
- 图存储:邻接表或父节点映射数组均占用 。
- 辅助结构:
visited数组、队列或递归栈,占用 。 - 总体:。
3. 优化建议:
- 针对 较大时:若 达到 ,应绝对避免使用
std::map<TreeNode*, TreeNode*>,而应利用题目给出的“权值唯一且范围小”的特点,直接使用数组下标作为索引。 - 针对 较大时:若 ,实际上可以直接判断输出空行。
- 内存连续性:在 NOI 竞赛中,使用静态邻接表(链式前向星)比
vector<vector<int>>具有更好的缓存命中率,速度更快。
易错点提示(教练寄语)
- 忘记双向边:很多同学只记住了左孩子和右孩子,忘了“向上”也是一条路。建立父节点索引是本题的灵魂。
- 死循环风险:在无向图中搜索必须使用
visited数组或判断from节点,否则会在父子之间来回横跳导致死循环。 - 排序要求:LeetCode 原题可能没要求排序,但 NOI 风格题目通常要求按序输出,务必审题。
- 的情况:当 时,结果应该是
target本身。BFS 和 DFS 逻辑通常能天然覆盖,但手动模拟时需留意。
- 1
信息
- ID
- 19454
- 时间
- 1000ms
- 内存
- 128MiB
- 难度
- 10
- 标签
- (无)
- 递交数
- 1
- 已通过
- 1
- 上传者