2 条题解

  • 0
    @ 2026-1-8 16:24:08

    这个错误是因为在之前的代码片段中,我使用了 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. 预备知识点总结 (针对本题)

    作为教练,我要求你在写代码前必须理解这三点:

    1. 二叉树的双向性:在普通的 DFS 中,我们只能“向下”走。要实现“向上”走,必须在输入时记录每个节点的父节点,或者像上面代码一样转化成无向图邻接表
    2. BFS 的层级特性:BFS 是一层一层扩散的,非常适合求解“距离为 K”的问题。
    3. 防环意识:在图遍历中,如果没有 vis 数组,你会在父节点和子节点之间来回跳跃,导致死循环。

    3. 读题关键词 (考场避坑)

    在 NOI 考场上,看到以下词汇要敏感:

    • “距离为 K” \rightarrow 暗示使用 BFS。
    • “权值互不相同” \rightarrow 说明可以直接用权值作为数组下标(如 adj[val]),不需要复杂的指针或哈希映射。
    • “所有结点” \rightarrow 暗示可能有多个答案,最后记得排序输出(NOI 风格通常要求升序)。

    4. 复杂度与优化建议

    • 时间复杂度O(N)O(N)。建图 O(N)O(N),BFS O(N)O(N),排序 O(NlogN)O(N \log N)。对于 N=500N=500 来说,绰绰有余。
    • 空间复杂度O(N)O(N)。主要开销在邻接表和 vis 数组。
    • 优化:如果你觉得 vector<int> adj[MAXN] 慢,可以使用 链式前向星(静态数组模拟邻接表),这是金牌选手在处理大数据量时的常用技巧。

    现在,你可以直接编译运行这段生成器代码,它将完美产生 10 组测试数据。

    • 0
      @ 2026-1-8 16:21:13

      在解决“树中距离为 KK 的节点”问题时,最关键的思维转变是:将“有向”的树结构视作“无向”的图结构

      以下提供三个版本的代码:从最基础的图转化法,到递归维护父节点的 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 竞赛级常数优化版(静态邻接表)

      思路: 在 NN 较大的情况下,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. 时间复杂度分析:

      • 建图过程:遍历 nn 个节点,每个节点处理 2 条边。复杂度 O(n)O(n)
      • 搜索过程:无论是 BFS 还是 DFS,由于每个节点最多被访问一次,每个邻接边最多被遍历一次。复杂度 O(n)O(n)
      • 排序输出O(nlogn)O(n \log n)
      • 总体O(nlogn)O(n \log n)。在 n=500n=500 的量级下,耗时通常小于 1ms。

      2. 空间复杂度分析:

      • 图存储:邻接表或父节点映射数组均占用 O(n)O(n)
      • 辅助结构visited 数组、队列或递归栈,占用 O(n)O(n)
      • 总体O(n)O(n)

      3. 优化建议:

      • 针对 nn 较大时:若 nn 达到 10510^5,应绝对避免使用 std::map<TreeNode*, TreeNode*>,而应利用题目给出的“权值唯一且范围小”的特点,直接使用数组下标作为索引。
      • 针对 kk 较大时:若 k>nk > n,实际上可以直接判断输出空行。
      • 内存连续性:在 NOI 竞赛中,使用静态邻接表(链式前向星)比 vector<vector<int>> 具有更好的缓存命中率,速度更快。

      易错点提示(教练寄语)

      1. 忘记双向边:很多同学只记住了左孩子和右孩子,忘了“向上”也是一条路。建立父节点索引是本题的灵魂。
      2. 死循环风险:在无向图中搜索必须使用 visited 数组或判断 from 节点,否则会在父子之间来回横跳导致死循环。
      3. 排序要求:LeetCode 原题可能没要求排序,但 NOI 风格题目通常要求按序输出,务必审题。
      4. k=0k=0 的情况:当 k=0k=0 时,结果应该是 target 本身。BFS 和 DFS 逻辑通常能天然覆盖,但手动模拟时需留意。
      • 1

      信息

      ID
      19454
      时间
      1000ms
      内存
      128MiB
      难度
      10
      标签
      (无)
      递交数
      1
      已通过
      1
      上传者