2 条题解

  • 0
    @ 2025-12-30 18:16:25

    作为教练,我为你提供一个符合 NOI 竞赛标准的高性能数据生成器。

    该生成器采用 C++14 编写,能够生成 10 组测试数据。为了确保数据的合法性,生成器采用 “先构树,再遍历” 的逻辑:先随机生成不同形态的二叉树,然后导出其前序和后序序列作为输入(.in),最后通过导出该树的中序序列作为标准答案(.out)。

    C++ 数据生成器 (gen.cpp)

    #include <iostream>
    #include <vector>
    #include <string>
    #include <fstream>
    #include <algorithm>
    #include <random>
    #include <stack>
    #include <ctime>
    
    using namespace std;
    
    // 定义树节点
    struct Node {
        int val;
        int l, r;
        Node() : val(0), l(-1), r(-1) {}
    };
    
    // 全局变量用于存储生成的树
    vector<Node> tree;
    vector<int> pre_seq, post_seq, in_seq;
    
    // --- 遍历序列生成函数 ---
    // 虽然 N=1000 递归没问题,但为了稳健,使用 std::vector 模拟
    void generate_sequences(int root) {
        if (root == -1) return;
        
        // 前序: 根 左 右
        pre_seq.push_back(tree[root].val);
        
        generate_sequences(tree[root].l);
        
        // 中序: 左 根 右 (作为答案)
        in_seq.push_back(tree[root].val);
        
        generate_sequences(tree[root].r);
        
        // 后序: 左 右 根
        post_seq.push_back(tree[root].val);
    }
    
    // --- 树构造逻辑 ---
    void make_tree(int n, int type, int seed) {
        tree.assign(n, Node());
        mt19937 rng(seed);
        
        // 生成 [1, n] 的随机排列作为节点值
        vector<int> vals(n);
        for (int i = 0; i < n; ++i) vals[i] = i + 1;
        shuffle(vals.begin(), vals.end(), rng);
        for (int i = 0; i < n; ++i) tree[i].val = vals[i];
    
        if (n <= 1) return;
    
        if (type == 1) { // 极端左斜链
            for (int i = 0; i < n - 1; ++i) tree[i].l = i + 1;
        } 
        else if (type == 2) { // 极端右斜链
            for (int i = 0; i < n - 1; ++i) tree[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[i].l = left;
                if (right < n) tree[i].r = right;
            }
        } 
        else { // 随机二叉树
            // 使用父亲选择法,保证连通且不超度数
            for (int i = 1; i < n; ++i) {
                int p = uniform_int_distribution<int>(0, i - 1)(rng);
                // 如果该父节点已满,向后寻找
                while (tree[p].l != -1 && tree[p].r != -1) {
                    p = (p + 1) % i;
                }
                if (tree[p].l == -1 && tree[p].r == -1) {
                    if (rng() % 2) tree[p].l = i; else tree[p].r = i;
                } else if (tree[p].l == -1) {
                    tree[p].l = i;
                } else {
                    tree[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);
    
        // 重置序列
        pre_seq.clear(); post_seq.clear(); in_seq.clear();
    
        // 1. 构造树并生成序列
        make_tree(n, type, id + 2025);
        generate_sequences(0);
    
        // 2. 写入 .in 文件
        fin << n << "\n";
        for (int i = 0; i < n; ++i) fin << pre_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 文件 (输出中序)
        for (int i = 0; i < n; ++i) fout << in_seq[i] << (i == n - 1 ? "" : " ");
        fout << "\n";
    
        fin.close(); fout.close();
        cout << "Test Case " << id << " [n=" << n << ", type=" << type << "] finished." << endl;
    }
    
    int main() {
        // 1-2: 边界测试 (单节点, 二节点)
        generate(1, 1, 0);
        generate(2, 2, 4);
    
        // 3-4: 极端链状 (测试递归深度和逻辑)
        generate(3, 1000, 1); // 左斜
        generate(4, 1000, 2); // 右斜
    
        // 5-6: 结构化树 (满二叉树)
        generate(5, 511, 3);
        generate(6, 1023, 3);
    
        // 7-10: 大规模随机树
        for (int i = 7; i <= 10; ++i) {
            generate(i, 1000, 4);
        }
    
        return 0;
    }
    

    数据说明及优化分析

    1. 文件大小控制

      • 本题目节点上限设为 N=1000N=1000(NOI 练习常见量级)。
      • 每个 .in 文件包含两行长度为 NN 的序列,.out 文件包含一行。
      • 每个整数平均 4 字节,全套 10 组数据总大小约为 120KB 左右,远低于 2MB。
    2. 生成逻辑的剪枝与效率

      • 树构造:在生成随机树时,采用了 (p+1)%i 的平滑寻址方式,避免了在父节点度数已满时的死循环,保证构造时间复杂度为 O(N2)O(N^2) 最坏,实际接近 O(N)O(N)。对于 N=1000N=1000 而言,生成速度极快。
      • 异常处理:所有随机数生成均使用 std::mt19937 配合分布器,不存在除以零的可能。
    3. 覆盖性设计

      • Case 3 & 4 (斜链):这是前序+后序构造最容易出错的地方,尤其是当节点只有一个孩子时,区间计算的偏移量极易写错。
      • Case 5 & 6 (完全二叉树):测试程序在平衡状态下的递归逻辑。
      • Case 7-10 (随机树):通过随机化左、右孩子的分配,全面覆盖各种非对称结构。

    辅导建议:

    • 关于“任意一个”解:由于前序+后序不能唯一确定树,选手的答案可能与生成器的 .out 不同但依然合法。在真实的 OJ 评测中,这道题通常需要配置 Special Judge (SPJ)
    • SPJ 逻辑提示:SPJ 应读取选手的输出构造出一棵树,然后检查这棵树的前序和后序遍历是否与输入一致。
    • 选手注意:提醒学生注意 N=1N=1 的特判,以及在递归中计算 left_size 时不要越界。

    你可以直接编译运行该生成器,它会自动在当前目录下产生所有测试点。祝你的 OJ 建设顺利!

    • 0
      @ 2025-12-30 18:14:17

      作为教练,我将带你从线性搜索递归版(逻辑最直观)过渡到哈希优化版(竞赛最优复杂度)。

      这道题的精髓在于:在前序序列中,根节点后面如果还有元素,那个元素必定是左子树的根;而在后序序列中,我们可以通过这个“左子树根”的位置,确定整个左子树的成员数量。


      版本一:基础递归版(线性搜索 O(N2)O(N^2)

      思路分析: 在每一次递归中,我们通过 for 循环在后序序列中寻找左子树根节点的位置。

      • 时间复杂度O(N2)O(N^2)。总共 NN 个节点,每个节点作为根都会扫描一次后序数组。
      • 空间复杂度O(N)O(N)。用于存储树结构和递归栈。
      • 评价:对于 N=30N=30 的数据量(原题范围),这个算法绰绰有余;对于 NOI 级别的 N=1000N=1000 以上,表现也尚可。
      #include <iostream>
      #include <vector>
      
      using namespace std;
      
      const int MAXN = 1005;
      
      // NOI 风格:使用静态数组存储树
      struct Node {
          int l, r, val;
      } tree[MAXN];
      
      int pre[MAXN], post[MAXN];
      int node_cnt = 0; // 节点计数器
      
      // preL, preR: 当前子树在前序序列中的区间
      // postL, postR: 当前子树在后序序列中的区间
      int build(int preL, int preR, int postL, int postR) {
          if (preL > preR) return -1; // 递归边界:空树
      
          int u = node_cnt++;
          tree[u].val = pre[preL];
      
          // 如果当前只有一个节点,说明是叶子,直接返回
          if (preL == preR) {
              tree[u].l = tree[u].r = -1;
              return u;
          }
      
          // 【核心逻辑】前序序列中,根节点后的第一个元素是左子树的根
          int left_root_val = pre[preL + 1];
          
          // 在后序序列中寻找左子树根的位置,从而确定左子树大小
          int idx = postL;
          while (idx < postR && post[idx] != left_root_val) {
              idx++;
          }
      
          int left_size = idx - postL + 1; // 左子树的节点个数
      
          // 递归构造左右子树
          // 左子树前序:跳过当前的根,取 left_size 个
          // 左子树后序:从 postL 开始到 idx
          tree[u].l = build(preL + 1, preL + left_size, postL, idx);
          
          // 右子树前序:跳过根和左子树部分
          // 右子树后序:左子树后到根之前
          tree[u].r = build(preL + left_size + 1, preR, idx + 1, postR - 1);
      
          return u;
      }
      
      // 题目要求输出中序遍历
      void inorder(int u, int n) {
          static int cnt = 0;
          if (u == -1) return;
          inorder(tree[u].l, n);
          cout << tree[u].val << (++cnt == n ? "" : " ");
          inorder(tree[u].r, n);
      }
      
      int main() {
          int n;
          if (!(cin >> n)) return 0;
          for (int i = 0; i < n; i++) cin >> pre[i];
          for (int i = 0; i < n; i++) cin >> post[i];
      
          int root_id = build(0, n - 1, 0, n - 1);
          inorder(root_id, n);
          cout << endl;
      
          return 0;
      }
      

      版本二:索引优化版(竞赛标准 O(N)O(N)

      思路分析: 为了消除内层循环,我们可以预处理后序序列,用一个数组 pos 记录每个值在后序序列中的下标。

      • 时间复杂度O(N)O(N)。预处理 O(N)O(N),递归中寻找位置 O(1)O(1)
      • 空间复杂度O(N)O(N)
      • 优化点:使用数组 pos 代替哈希表 unordered_map,在竞赛中常数更小,速度更快。
      #include <iostream>
      
      using namespace std;
      
      const int MAXN = 1005;
      int pre[MAXN], post[MAXN];
      int pos[MAXN]; // 记录 post[i] 的值在哪个下标
      int node_val[MAXN], L[MAXN], R[MAXN];
      int node_ptr = 0;
      
      int build(int preL, int preR, int postL, int postR) {
          if (preL > preR) return -1;
          
          int u = node_ptr++;
          node_val[u] = pre[preL];
          
          // 如果是叶子,直接返回
          if (preL == preR) {
              L[u] = R[u] = -1;
              return u;
          }
      
          // 通过预处理好的 pos 数组,$O(1)$ 时间获取左子树根的位置
          int left_root_val = pre[preL + 1];
          int idx = pos[left_root_val];
          
          int left_size = idx - postL + 1;
      
          L[u] = build(preL + 1, preL + left_size, postL, idx);
          R[u] = build(preL + left_size + 1, preR, idx + 1, postR - 1);
      
          return u;
      }
      
      void inorder_print(int u, int n) {
          static int cnt = 0;
          if (u == -1) return;
          inorder_print(L[u], n);
          cout << node_val[u] << (++cnt == n ? "" : " ");
          inorder_print(R[u], n);
      }
      
      int main() {
          // NOI 技巧:加速 I/O
          ios::sync_with_stdio(false);
          cin.tie(0);
      
          int n;
          if (!(cin >> n)) return 0;
          for (int i = 0; i < n; i++) cin >> pre[i];
          for (int i = 0; i < n; i++) {
              cin >> post[i];
              pos[post[i]] = i; // 记录位置
          }
      
          int root = build(0, n - 1, 0, n - 1);
          inorder_print(root, n);
          cout << endl;
      
          return 0;
      }
      

      复杂度分析思考过程

      1. 时间复杂度思考

        • 在还原树的过程中,每一个节点都恰好被作为根节点处理一次。
        • 关键操作是“确定子树分界点”。在 O(N2)O(N^2) 版本中,这个操作是不稳定的。
        • 通过预处理 pos 数组,我们将寻找分界点的代价降到了常数。总复杂度 T(N)=O(N)T(N) = O(N)
      2. 空间复杂度思考

        • 存储空间:我们需要存储树的结构(左、右孩子和值),需要 3N3N 的空间。
        • 递归栈空间:取决于树的高度。最坏情况(树退化为链)为 O(N)O(N)。在 NOI 竞赛中,通常系统栈为 256MB 或更大,对于 N=1000N=1000 绝对安全。
      3. 时间复杂度优化建议

        • 避免使用 std::map:在节点值为 1N1 \dots N 时,数组映射总是比哈希表快。
        • 静态化存储:在 C++ 中,直接开数组(如 L[MAXN])通常比 vector 或指针对象性能更优,且更不容易产生内存碎片。

      读题关键词与易错点(教练总结)

      • “任意一个” (Any)
        • 这是本题最大的陷阱。由于没有中序遍历,当一个节点只有一个子节点时,我们无法分辨它是左还是右。
        • 我们的代码默认将 pre[preL + 1] 识别为左子树根,也就是在这种情况下,我们默认它是左孩子
      • “左子树规模” (Size)
        • 切割区间的核心是 size
        • 记住公式:左子树在后序序列中结束于 idx(左子树根所在的位置),所以大小是 idx - postL + 1
      • 边界条件 preL == preR
        • 在尝试读取 pre[preL + 1] 之前,必须判断当前节点是否为叶子节点,否则会发生数组越界。

      练习建议: 既然学会了前序+后序,请立刻复习一下“前序+中序”和“中序+后序”。你会发现,中序遍历才是唯一确定树形态的“定海神针”。没有了中序,树的形态就会产生歧义。加油!

      • 1

      信息

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