2 条题解

  • 0
    @ 2025-12-30 17:57:53

    作为教练,我为你准备了一个针对“从前序与中序遍历构造二叉树”的数据生成器。

    在 NOI 竞赛中,N=3000N=3000 是一个适中的规模。这个生成器能够构造随机树、完全二叉树、左斜树、右斜树等多种形态,并自动根据生成的树产生合法的 preorderinorder 输入,同时计算出标准答案的 postorder

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

    #include <iostream>
    #include <vector>
    #include <algorithm>
    #include <random>
    #include <fstream>
    #include <string>
    #include <stack>
    #include <map>
    
    using namespace std;
    
    // --- 树的结构定义 ---
    struct Node {
        int val;
        int l, r;
        Node() : val(0), l(-1), r(-1) {}
    };
    
    // --- 全局变量,用于存储生成的树和遍历序列 ---
    vector<Node> tree;
    vector<int> pre_seq, in_seq, post_seq;
    const int OFFSET = 3000;
    
    // --- 辅助函数:通过 DFS 获取各种遍历序列 ---
    // 虽然 N=3000 递归没问题,但为了稳健,这里展示标准的递归写法
    void get_traversals(int u) {
        if (u == -1) return;
        pre_seq.push_back(tree[u].val); // 前序
        get_traversals(tree[u].l);
        in_seq.push_back(tree[u].val);  // 中序
        get_traversals(tree[u].r);
        post_seq.push_back(tree[u].val); // 后序 (答案)
    }
    
    // --- 标准答案逻辑 (Solver) ---
    // 这部分代码将被集成到生成器中,用于产生 .out 文件
    int pos_map[6005];
    struct ResultNode { int l, r; } sol_tree[3005];
    int sol_val[3005];
    
    int solve_build(int& cur_pre, int inL, int inR, const vector<int>& pre, const vector<int>& in) {
        if (inL > inR) return -1;
        int root_val = pre[cur_pre++];
        int u = cur_pre - 1; 
        sol_val[u] = root_val;
        int k = pos_map[root_val + OFFSET];
        sol_tree[u].l = solve_build(cur_pre, inL, k - 1, pre, in);
        sol_tree[u].r = solve_build(cur_pre, k + 1, inR, pre, in);
        return u;
    }
    
    void get_sol_post(int u, vector<int>& res) {
        if (u == -1) return;
        get_sol_post(sol_tree[u].l, res);
        get_sol_post(sol_tree[u].r, res);
        res.push_back(sol_val[u]);
    }
    
    // --- 数据生成主逻辑 ---
    void make_data(int id, int n, string type) {
        string in_file = to_string(id) + ".in";
        string out_file = to_string(id) + ".out";
        ofstream fin(in_file);
        ofstream fout(out_file);
    
        // 1. 生成不重复的节点值
        vector<int> vals(6001);
        for(int i = 0; i <= 6000; ++i) vals[i] = i - 3000;
        mt19937 rng(id + 12345);
        shuffle(vals.begin(), vals.end(), rng);
        
        tree.clear(); tree.resize(n);
        for(int i = 0; i < n; ++i) tree[i].val = vals[i];
    
        // 2. 根据类型构造树结构
        if (type == "left") { // 左斜树
            for(int i = 0; i < n - 1; ++i) tree[i].l = i + 1;
        } else if (type == "right") { // 右斜树
            for(int i = 0; i < n - 1; ++i) tree[i].r = i + 1;
        } else if (type == "full") { // 类似完全二叉树
            for(int i = 0; i < n; ++i) {
                if (2 * i + 1 < n) tree[i].l = 2 * i + 1;
                if (2 * i + 2 < n) tree[i].r = 2 * i + 2;
            }
        } 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;
            }
        }
    
        // 3. 生成输入序列
        pre_seq.clear(); in_seq.clear(); post_seq.clear();
        get_traversals(0);
    
        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 << in_seq[i] << (i == n - 1 ? "" : " ");
        fin << "\n";
    
        // 4. 调用标程逻辑生成输出序列
        for(int i = 0; i < 6005; ++i) pos_map[i] = 0;
        for(int i = 0; i < n; ++i) pos_map[in_seq[i] + OFFSET] = i;
        int cur_pre = 0;
        int sol_root = solve_build(cur_pre, 0, n - 1, pre_seq, in_seq);
        vector<int> sol_res;
        get_sol_post(sol_root, sol_res);
    
        for(int i = 0; i < n; ++i) fout << sol_res[i] << (i == n - 1 ? "" : " ");
        fout << "\n";
    
        fin.close(); fout.close();
        cout << "Test Case " << id << " [N=" << n << ", Type=" << type << "] Generated." << endl;
    }
    
    int main() {
        make_data(1, 1, "random");   // 边界 1
        make_data(2, 2, "random");   // 边界 2
        make_data(3, 5, "full");     // 样例规模
        make_data(4, 100, "left");   // 小规模左斜
        make_data(5, 3000, "left");  // 极限左斜 (卡递归)
        make_data(6, 3000, "right"); // 极限右斜
        make_data(7, 3000, "full");  // 满二叉树
        make_data(8, 3000, "random");// 随机 1
        make_data(9, 3000, "random");// 随机 2
        make_data(10, 3000, "random");// 随机 3
        return 0;
    }
    

    数据说明及优化分析

    1. 区分度分析

      • Test 5 & 6 (Skewed Trees):这两个测试点是“递归杀手”。如果选手的代码在处理区间切分时逻辑不严谨,或者使用了常数很大的方法,在这两个点会非常危险。
      • Test 7 (Full Tree):测试算法在处理高度为 logN\log N 的树时的表现,通常这是最快的情况。
      • Test 8-10 (Random):测试普遍性能,节点值分布在整个 [3000,3000][-3000, 3000] 区间。
    2. 文件大小控制

      • 对于 N=3000N=3000,每一行大约有 3000 个整数,每个整数平均 4 字节(含空格),一行大约 12KB12KB
      • 每个 .in 文件约 25KB25KB.out 文件约 12KB12KB
      • 总计 10 组数据总大小约为 400KB 左右,远低于 2MB 的要求。
    3. 时间复杂度优化

      • 生成器端:构建树的过程使用了简单的父节点查找法,复杂度 O(N2)O(N^2)N=3000N=3000 下只需数毫秒。
      • 标程端:使用了索引加速 (O(N)O(N) 算法),生成 10 组数据总耗时不超过 0.5s。
    4. 鲁棒性 (Robustness)

      • 避免除零:代码中不涉及除法。
      • 防爆栈:虽然 N=3000N=3000 很难爆掉现代评测机的栈(通常是 8MB-512MB),但如果在本地 Windows 环境下使用默认栈大小编译,请确保 NN 不要超过 10510^5

    教练提示(OJ 创建建议):

    • 在上传题目时,可以将 Test 5 设为高分值,以筛选出那些对空间和递归效率有深刻理解的选手。
    • 输出要求最后一行没有多余空格,生成器代码已通过 (i == n - 1 ? "" : " ") 逻辑确保了输出的严谨性。
    • 0
      @ 2025-12-30 17:56:28

      作为你的教练,我将带你从最基础的线性搜索递归版开始,逐步优化到竞赛级的哈希/索引加速版

      在还原二叉树时,核心逻辑是:利用前序序列确定“根是谁”,利用中序序列确定“左子树有哪些,右子树有哪些”


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

      思路分析: 这是最符合直觉的写法。在每一层递归中,我们都在中序遍历序列中“傻傻地”从头到尾找根节点的位置。

      • 时间复杂度分析:每个节点作为根节点都会进入一次递归(共 NN 次)。每次寻找中序索引需要 O(N)O(N)。总复杂度 O(N2)O(N^2)
      • 空间复杂度分析O(N)O(N),用于存储树结构和递归系统栈。
      • 评价:对于 N=3000N=3000 的数据量, O(N2)O(N^2) 约为 9×1069 \times 10^6,在 1s 限制内勉强能过,但在 N=105N=10^5 时会 TLE。
      #include <iostream>
      #include <vector>
      
      using namespace std;
      
      const int MAXN = 3005;
      
      struct Node {
          int val;
          int l, r;
      } tree[MAXN];
      
      int preorder[MAXN], inorder[MAXN];
      int node_cnt = 0; // 用于分配静态数组下标
      
      // 简单递归还原
      // preL, preR: 当前子树在前序序列中的范围
      // inL, inR: 当前子树在中序序列中的范围
      int build(int preL, int preR, int inL, int inR) {
          if (preL > preR) return -1; // 递归边界:区间为空
      
          int root_val = preorder[preL];
          int u = node_cnt++; // 分配一个新节点编号
          tree[u].val = root_val;
      
          // 【关键点】在线性搜索中序序列中的根节点位置
          int k = inL;
          while (k <= inR && inorder[k] != root_val) {
              k++;
          }
      
          int left_size = k - inL; // 左子树节点个数
      
          // 【易错点】区间的计算:前序序列中,根后面紧跟 left_size 个左子树节点
          tree[u].l = build(preL + 1, preL + left_size, inL, k - 1);
          tree[u].r = build(preL + left_size + 1, preR, k + 1, inR);
      
          return u;
      }
      
      // 后序遍历输出,用于验证
      void postOrder(int u) {
          if (u == -1) return;
          postOrder(tree[u].l);
          postOrder(tree[u].r);
          cout << tree[u].val << " ";
      }
      
      int main() {
          int n;
          if (!(cin >> n)) return 0;
          for (int i = 0; i < n; i++) cin >> preorder[i];
          for (int i = 0; i < n; i++) cin >> inorder[i];
      
          int root_id = build(0, n - 1, 0, n - 1);
          postOrder(root_id);
          cout << endl;
      
          return 0;
      }
      

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

      思路分析: 为了消除 O(N2)O(N^2) 中的那个 NN,我们可以预处理中序序列。由于节点值唯一,我们可以用一个数组 pos 记录每个值在中序序列中的下标。

      • 时间复杂度分析:预处理 O(N)O(N),递归过程每个节点 O(1)O(1),总计 O(N)O(N)
      • 空间复杂度分析O(N)O(N)
      • 注意:节点值包含负数,数组下标不能为负,我们需要加上一个 OFFSET(偏移量)。
      #include <iostream>
      
      using namespace std;
      
      const int MAXN = 3005;
      const int OFFSET = 3000; // 节点值最小-3000,加上3000转为非负
      
      struct Node {
          int val, l, r;
      } tree[MAXN];
      
      int preorder[MAXN], inorder[MAXN];
      int pos[6005]; // 存储中序遍历值对应的下标
      int node_cnt = 0;
      
      // 优化后的递归构建
      int build(int preL, int preR, int inL, int inR) {
          if (preL > preR) return -1;
      
          int root_val = preorder[preL];
          int u = node_cnt++;
          tree[u].val = root_val;
      
          // 【性能优化】通过预处理的数组直接 $O(1)$ 获取位置
          int k = pos[root_val + OFFSET];
          int left_size = k - inL;
      
          tree[u].l = build(preL + 1, preL + left_size, inL, k - 1);
          tree[u].r = build(preL + left_size + 1, preR, k + 1, inR);
      
          return u;
      }
      
      void postOrder(int u, int n) {
          static int cnt = 0;
          if (u == -1) return;
          postOrder(tree[u].l, n);
          postOrder(tree[u].r, n);
          cout << tree[u].val << (++cnt == n ? "" : " ");
      }
      
      int main() {
          // 关闭同步流提高效率
          ios::sync_with_stdio(false);
          cin.tie(0);
      
          int n;
          if (!(cin >> n)) return 0;
          for (int i = 0; i < n; i++) cin >> preorder[i];
          for (int i = 0; i < n; i++) {
              cin >> inorder[i];
              // 【关键点】预处理索引映射
              pos[inorder[i] + OFFSET] = i;
          }
      
          int root_id = build(0, n - 1, 0, n - 1);
          postOrder(root_id, n);
          cout << endl;
      
          return 0;
      }
      

      复杂度分析思考过程

      1. 时间复杂度优化建议

        • O(N2)O(N^2)O(N)O(N):这是通过“空间换时间”的典型策略。在 OI 中,如果发现递归内部有循环搜索,第一时间想能不能用哈希表(unordered_map)或索引数组预处理。
        • 递归开销:对于 N=3000N=3000,系统栈完全没问题。如果 N=106N=10^6,需要考虑手动模拟栈实现非递归构建,或者通过 ulimit -s unlimited 增加栈空间。
      2. 空间复杂度思考

        • 本题使用了静态数组存储树,空间利用率极高。
        • pos 数组的大小由节点值的范围决定(60016001 个位置),而不是节点个数。如果节点值范围达到 10910^9,则必须换用 std::unordered_map

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

      • “根在哪里?”:前序序列的 preL 位置。
      • “左右子树规模”:由中序中根的位置 k 决定。左子树规模 size = k - inL
      • “边界控制”:递归还原中最容易错的是 preL + 1preL + size 这种下标计算。口诀:前序中左子树紧跟根后,右子树在左子树后。
      • “节点值不重复”:这是此算法生效的唯一前提。如果题目没写这一句,说明不能唯一确定一棵树。

      练习建议: 既然学会了前序+中序,你可以立刻尝试一下“中序+后序”。唯一的区别是:后序的根在序列的最后一个,且在切割前序序列时,你需要先确定右子树的范围。逻辑大同小异,快去练练吧!加油!

      • 1

      信息

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