2 条题解

  • 0
    @ 2025-12-30 16:19:09

    为了方便你快速创建 OJ(Online Judge)测试数据,我编写了这个高性能的数据生成器。它采用 C++14 标准,结合了 BFS 迭代算法 生成标准输出,确保即使在极端链状数据下也不会爆栈。

    数据生成器设计思路

    1. 分级测试点设计
      • 1.in - 2.in边界情况。空树 (n=0n=0) 和单节点树 (n=1n=1)。
      • 3.in小规模构造。手动模拟 LeetCode 示例 2 的复杂树结构。
      • 4.in极端深度(链状)n=5000n=5000,每个节点只有一个孩子,测试算法对深度的处理。
      • 5.in极端宽度(星形)n=5000n=5000,根节点连接所有其他节点,测试算法对单层极宽情况的处理。
      • 6.in - 10.in大规模随机树nn 增加至 1000010000,模拟各种分叉因子(Binary, Ternary, High-degree)。
    2. 安全性与性能
      • 非递归生成答案:使用 std::queue 实现 BFS,彻底规避递归导致的系统栈溢出。
      • 高效构树:通过 p = random(0, i-1) 确保生成的图一定是连通且无环的树。
      • 文件大小优化10410^4 级别的节点数据,单个文件约 150KB200KB150KB \sim 200KB,总数据量远小于 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;
    }
    

    数据说明及优化点

    1. 防止除以 0 异常
      • 在随机父节点选择时,使用了 uniform_int_distribution<int>(0, i-1)。由于循环从 i=1 开始,区间下界永远不大于上界,且不涉及任何除法操作,安全性极高。
    2. 文件存储优化
      • 采用了标准的 ofstream 流。
      • 在生成大规模随机树时,使用了 vector::reserve 来减少内存碎片,保证生成器在几百毫秒内即可完成所有数据的制作。
    3. 为何选择 N=10000N=10000
      • 对于 O(N)O(N) 算法,10410^4 能在瞬间跑完。
      • 对于选手的 O(N2)O(N^2) 或更差的算法(例如每次都在数组中通过搜索来找节点),这个数据量配合 1s 时间限制能产生明显的区分度。
    4. 读取建议
      • .out 文件的第一行增加了深度 LL。这在 OJ 评测中非常有用,可以方便判题脚本或选手确认输出规模。

    你可以直接在本地编译运行该代码,它会在当前目录下生成 1.in/out10.in/out。加油,祝你的 OJ 建设顺利!

    • 0
      @ 2025-12-30 16:13:03

      作为教练,我会带你从最容易想到的深度优先搜素(DFS)辅助法,逐步进化到竞赛中最稳健的标准广度优先搜索(BFS),最后通过手动模拟队列与 I/O 优化达到最优性能。


      版本一:辅助 DFS 法(简单直观,但非标准 BFS)

      思路分析: 虽然题目要求层序遍历,但我们可以利用 DFS 遍历整棵树,同时在递归过程中记录每个节点的“深度”。我们将同一深度的节点存入同一个 vector

      • 时间复杂度O(N)O(N),每个节点访问一次。
      • 空间复杂度O(N)O(N),用于存储答案和递归栈(最坏情况 O(N)O(N))。
      #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 就是当前层的所有节点数量。

      • 时间复杂度O(N)O(N)
      • 空间复杂度O(N)O(N),队列最大可能容纳一层的所有节点(O(N)O(N))。
      #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;
      }
      

      版本三:最优复杂度(手动队列 + 链式存储思路)

      思路分析: 在数据量极大时,O(N)O(N) 虽然已经最优,但 std::vector 的内存分配和 std::queue 的封装开销可以通过手动模拟来消除。我们使用数组模拟队列,并直接在结果输出时处理。

      • 时间复杂度O(N)O(N),常数极小。
      • 空间复杂度O(N)O(N),利用数组预分配。
      #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;
      }
      

      复杂度分析与性能建议

      1. 时间复杂度分析思考过程

        • 层序遍历实际上是广度优先搜索的一种。由于每个节点都会且仅会入队一次,所有边也会被扫描一次。
        • 总时间复杂度为 O(V+E)O(V + E),在树结构中 E=V1E = V-1,因此是标准的 O(N)O(N)
        • 优化建议:在处理每一层的 vector 时,如果不关心层级,可以不存储直接输出。但题目要求按层输出,所以存储是必须的。
      2. 空间复杂度分析思考过程

        • 存储空间:邻接表需要 O(N)O(N)
        • 辅助空间:队列在最坏情况下需要 O(N)O(N)(比如根节点有 N1N-1 个孩子)。
        • 优化建议:如果内存限制极其严格(例如 64MB 处理 10610^6 级别数据),建议舍弃 std::vector 存储邻接表,改用链式前向星(使用静态数组模拟链表)。
      3. 读题关键词与易错点

        • “层序”:暗示 BFS。
        • “从左到右”:读入孩子节点时,必须按顺序 push_back,不能乱序。
        • n=0n=0 情况:NOI 题目经常会给 n=0n=0 的测试点,如果代码中直接 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
      上传者