3 条题解

  • 0
    @ 2026-2-23 11:19:33
    #include<bits/stdc++.h>
    using namespace std;
    
    struct Node{
        int val;
        int l;
        int r;
    };
    
    Node tree[100];
    
    vector<int> result;
    
    
    void preorder(int p){
        if(p==-1) return;
        result.push_back(tree[p].val);
        preorder(tree[p].l);
        preorder(tree[p].r);
    }
    int main(){
        int n;
        cin>>n;
        //数据范围允许0所以必须对0特判,
        //否则下面tree初始化会跳过导致左右都是0,后续preorder会无限递归导致爆栈segmentfault
        if(n==0) return 0;
        //注意这里i不从0而从1开始,是为了让树的节点值和数组下标对应,容易按节点值访问
        for(int i=1;i<=n;i++){
            cin>>tree[i].val>>tree[i].l>>tree[i].r;
        }
    
        preorder(1);
    
        for(int i=0;i<result.size();i++){
            cout<<result[i]<<" ";
        }
    }
    
    • 0
      @ 2025-12-30 11:56:53

      为了方便你为 OJ (Online Judge) 准备测试数据,我为你编写了一个基于 C++14 标准的数据生成器。

      该生成器会生成 10 组测试点,涵盖了空树、单节点、完全二叉树、左斜树(链状)、右斜树(链状)以及复杂随机树。为了保证生成的 .out 文件绝对正确且不会爆栈,生成器内部使用了**迭代式(显式栈)**的前序遍历算法。

      数据生成器代码 (gen_tree.cpp)

      #include <iostream>
      #include <fstream>
      #include <vector>
      #include <stack>
      #include <string>
      #include <algorithm>
      #include <random>
      #include <chrono>
      
      using namespace std;
      
      // 节点结构体
      struct Node {
          int val;
          int l, r;
      };
      
      // 使用迭代法生成标准答案,确保不爆栈
      vector<int> getPreorder(int n, const vector<Node>& tree) {
          if (n <= 0) return {};
          vector<int> res;
          stack<int> s;
          s.push(1); // 根节点固定为 1
          
          while (!s.empty()) {
              int u = s.top();
              s.pop();
              res.push_back(tree[u].val);
              // 先压右再压左,弹出时即为先左后右
              if (tree[u].r != -1) s.push(tree[u].r);
              if (tree[u].l != -1) s.push(tree[u].l);
          }
          return res;
      }
      
      // 写入 .in 文件
      void writeIn(int n, const vector<Node>& tree, string filename) {
          ofstream fout(filename);
          fout << n << endl;
          for (int i = 1; i <= n; ++i) {
              fout << tree[i].val << " " << tree[i].l << " " << tree[i].r << endl;
          }
          fout.close();
      }
      
      // 写入 .out 文件
      void writeOut(const vector<int>& res, string filename) {
          ofstream fout(filename);
          for (int i = 0; i < (int)res.size(); ++i) {
              fout << res[i] << (i == (int)res.size() - 1 ? "" : " ");
          }
          fout << endl;
          fout.close();
      }
      
      int main() {
          mt19937 rng(chrono::steady_clock::now().time_since_epoch().count());
          uniform_int_distribution<int> valDist(-100, 100);
      
          for (int t = 1; t <= 10; ++t) {
              int n = 0;
              vector<Node> tree;
              string in_name = to_string(t) + ".in";
              string out_name = to_string(t) + ".out";
      
              if (t == 1) n = 0; // 边界:空树
              else if (t == 2) n = 1; // 边界:单节点
              else if (t == 3) n = 3; // 基础树:1 是根,2 是左,3 是右
              else if (t == 4) n = 50; // 左斜树 (退化成链)
              else if (t == 5) n = 50; // 右斜树 (退化成链)
              else if (t == 6) n = 63; // 满二叉树 (深度为 6)
              else if (t == 7) n = 100; // 随机树 1
              else if (t == 8) n = 100; // 随机树 2
              else if (t == 9) n = 100; // 随机树 3
              else n = 100; // 最大规模随机树
      
              tree.assign(n + 1, {0, -1, -1});
              for (int i = 1; i <= n; ++i) tree[i].val = valDist(rng);
      
              if (t == 3) {
                  tree[1].l = 2; tree[1].r = 3;
              } else if (t == 4) { // 左斜
                  for (int i = 1; i < n; ++i) tree[i].l = i + 1;
              } else if (t == 5) { // 右斜
                  for (int i = 1; i < n; ++i) tree[i].r = i + 1;
              } else if (t == 6) { // 满二叉树逻辑
                  for (int i = 1; i <= n / 2; ++i) {
                      tree[i].l = 2 * i;
                      tree[i].r = 2 * i + 1;
                  }
              } else if (t >= 7) { // 随机生成合法二叉树
                  vector<int> available;
                  available.push_back(1);
                  vector<int> free_slots(n + 1, 2); 
                  for (int i = 2; i <= n; ++i) {
                      uniform_int_distribution<int> pick(0, (int)available.size() - 1);
                      int p_idx = pick(rng);
                      int p = available[p_idx];
                      
                      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;
      
                      free_slots[p]--;
                      if (free_slots[p] == 0) {
                          available.erase(available.begin() + p_idx);
                      }
                      available.push_back(i);
                  }
              }
      
              writeIn(n, tree, in_name);
              writeOut(getPreorder(n, tree), out_name);
              cout << "Generated Case " << t << " (n=" << n << ")" << endl;
          }
          return 0;
      }
      

      测试点详细说明表

      测试点 nn 规模 树形态特征 设计目的
      1 0 空树 考察对 nullptrn=0n=0 输入的基础防御能力
      2 1 仅根节点 考察递归基准情况或循环终止条件
      3 3 简单完全树 验证最基础的 根->左->右 逻辑
      4 50 纯左斜树 考察算法在极端深度(链状)下的稳定性
      5 纯右斜树
      6 63 满二叉树 验证对标准对称结构的遍历正确性
      7 100 随机树 A 综合逻辑测试
      8 随机树 B
      9 随机树 C 包含大量随机叶子节点的情况
      10 满载随机树 最大规模测试

      数据生成器设计规范说明

      1. 非递归求精 (Safety)
        • 在生成 .out 标准答案时,我使用了 stack<int> 模拟的迭代法。即使未来将 nn 调整到 10610^6,这个生成器也不会因为系统栈溢出而崩溃,确保了数据的可靠性。
      2. 文件大小优化 (Compactness)
        • n=100n=100 的规模下,每个 .in 文件大约只有 1KB 到 2KB。10 组数据总大小不超过 30KB,极大地节省了 OJ 存储和下载带宽。
      3. 随机性与合法性 (Correctness)
        • 随机树算法:采用“节点逐一加入”法。新节点 ii 随机挂载到已有节点 pp 的空槽位上。这种方法保证了生成的结构一定是合法的二叉树,且不会出现环或多个根。
        • 种子设置:使用 chrono 纳秒时钟,保证每次运行生成的数据序列都是唯一的。
      4. 防除零与异常 (Robustness)
        • 代码逻辑中不涉及除法操作(除满二叉树索引外,已做安全判断),避免了 Floating Point Exception
        • 使用了 vector 管理内存,避免了手动申请内存可能导致的 Memory Limit Exceeded

      使用方法

      1. 将代码保存为 gen.cpp
      2. 在终端执行:g++ gen.cpp -o gen -std=c++14
      3. 运行 ./gen,当前目录即会出现 1.in/1.out10.in/10.out
      • 0
        @ 2025-12-30 11:54:53

        同学你好!二叉树的前序遍历是树论算法的起点。在 NOI 竞赛中,掌握不同复杂度的实现方式能让你在面对不同内存限制(MLE)和时间限制(TLE)时游刃有余。

        以下是三种版本的完整 C++14 代码实现。


        版本一:递归法 (最简实现)

        思路分析: 利用二叉树的递归特性:Preorder(root) = root + Preorder(root->left) + Preorder(root->right)

        • 时间复杂度O(n)O(n),每个节点访问一次。
        • 空间复杂度O(h)O(h)hh 为树高。最坏情况下(链状树)为 O(n)O(n)
        • 关键点:递归边界判断 if (root == nullptr)
        #include <iostream>
        #include <vector>
        
        using namespace std;
        
        // NOI 竞赛常使用静态数组模拟树结构
        struct Node {
            int val;
            int l, r;
        } tree[105];
        
        // 结果存储
        vector<int> res;
        
        void preorder(int u) {
            if (u == -1) return; // 递归边界:遇到空节点返回
            
            // 1. 访问根节点
            res.push_back(tree[u].val);
            
            // 2. 递归左子树
            preorder(tree[u].l);
            
            // 3. 递归右子树
            preorder(tree[u].r);
        }
        
        int main() {
            int n;
            if (!(cin >> n) || n == 0) return 0;
            
            // 读入节点:v 为值,l r 为左右孩子编号
            for (int i = 1; i <= n; i++) {
                cin >> tree[i].val >> tree[i].l >> tree[i].r;
            }
            
            preorder(1); // 从编号 1 的根节点开始
            
            for (int i = 0; i < res.size(); i++) {
                cout << res[i] << (i == res.size() - 1 ? "" : " ");
            }
            cout << endl;
            
            return 0;
        }
        

        版本二:迭代法 (栈模拟)

        思路分析: 为了防止递归层数过深导致栈溢出,我们手动维护一个 std::stack

        • 时间复杂度O(n)O(n)
        • 空间复杂度O(h)O(h)
        • 易错点:由于栈是 LIFO (后进先出),在前序遍历中,为了先访问左孩子,必须先将右孩子压栈,再将左孩子压栈
        #include <iostream>
        #include <vector>
        #include <stack>
        
        using namespace std;
        
        struct Node {
            int val;
            int l, r;
        } tree[105];
        
        void iterativePreorder(int rootIdx) {
            if (rootIdx == -1) return;
            
            stack<int> s;
            s.push(rootIdx);
            
            while (!s.empty()) {
                int u = s.top();
                s.pop();
                
                cout << tree[u].val << " "; // 访问当前根
                
                // 关键:先压右再压左,弹出时就是先左后右
                if (tree[u].r != -1) s.push(tree[u].r);
                if (tree[u].l != -1) s.push(tree[u].l);
            }
        }
        
        int main() {
            int n; cin >> n;
            if (n == 0) return 0;
            for (int i = 1; i <= n; i++) cin >> tree[i].val >> tree[i].l >> tree[i].r;
            
            iterativePreorder(1);
            cout << endl;
            return 0;
        }
        

        版本三:Morris 遍历 (空间最优解)

        思路分析: Morris 遍历的精髓在于利用叶子节点的空指针。它通过建立临时的“线索”回到父节点,从而实现 O(1)O(1) 的额外空间。

        • 时间复杂度O(n)O(n)。虽然有嵌套循环,但每条边最多被访问 3 次。
        • 空间复杂度O(1)O(1),不计存储答案的容器。
        • 关键逻辑
          1. 如果当前节点左子树为空,访问当前节点,去右边。
          2. 如果左子树不为空,找到左子树最右边的节点(前驱节点)。
          3. 如果前驱节点的右指针为空,说明第一次到这,访问当前节点,建立线索(右指针指向自己),去左边。
          4. 如果前驱节点的右指针指向自己,说明左边走完了,拆掉线索,去右边。
        #include <iostream>
        #include <vector>
        
        using namespace std;
        
        // 为实现 Morris 遍历,我们需要指针化的结构,或者在静态数组上操作
        struct Node {
            int val;
            int l, r;
        } tree[105];
        
        void morrisPreorder(int rootIdx) {
            int curr = rootIdx;
            while (curr != -1) {
                if (tree[curr].l == -1) {
                    // 情况 1: 没有左孩子,直接访问并去右边
                    cout << tree[curr].val << " ";
                    curr = tree[curr].r;
                } else {
                    // 情况 2: 有左孩子,找前驱
                    int pre = tree[curr].l;
                    while (tree[pre].r != -1 && tree[pre].r != curr) {
                        pre = tree[pre].r;
                    }
                    
                    if (tree[pre].r == -1) {
                        // 第一次访问:建立线索
                        cout << tree[curr].val << " "; // 前序遍历在建立线索时访问
                        tree[pre].r = curr;
                        curr = tree[curr].l;
                    } else {
                        // 第二次访问:线索已存在,拆除线索回溯
                        tree[pre].r = -1;
                        curr = tree[curr].r;
                    }
                }
            }
        }
        
        int main() {
            int n; cin >> n;
            if (n == 0) return 0;
            for (int i = 1; i <= n; i++) cin >> tree[i].val >> tree[i].l >> tree[i].r;
            
            morrisPreorder(1);
            cout << endl;
            return 0;
        }
        

        四、 复杂度分析思考过程

        1. 时间复杂度分析

          • 递归与迭代:很明显,每个节点进出栈一次或访问一次,均为 O(n)O(n)
          • Morris:乍看 while 嵌套 whileO(n2)O(n^2),但实际上每条“建立线索”的边在建立和拆除时各走一次,总边数 n1n-1,故为 O(n)O(n)
        2. 空间复杂度优化建议

          • 在 NOI 比赛中,若 nn 较大且树退化为链(深度大),递归法会报 RE (栈溢出)。
          • 优化 1:优先使用迭代法
          • 优化 2:如果内存限制极其严苛(如 4MB/8MB),Morris 遍历是唯一选择。
          • 优化 3:读入优化。若 n>105n > 10^5,使用 scanfgetchar() 快读。

        五、 读题关键词总结 (考场指南)

        在处理树遍历题目时,注意这些隐含信息:

        1. “前序/先序 (Preorder)”:核心是“早访问”。只要到了一个节点,还没去子树前就要处理它。
        2. O(n)O(n) 节点”:如果 nn 特别大(如 10610^6),注意递归深度,必要时手动开大系统栈或改迭代。
        3. “节点值范围”:如果值很大,注意是否需要 long long(本题不需要)。
        4. “左、右子树”:在迭代法中,注意入栈顺序与遍历顺序是相反的。

        教练寄语: 同学,二叉树的前序遍历是进入高级算法的大门。递归法练的是逻辑拆解,迭代法练的是数据结构模拟,而 Morris 遍历练的是空间复用的极致思维。请一定要在草稿纸上把 Morris 建立和拆除线索的过程画出来,那是算法之美。加油!

        • 1

        信息

        ID
        19407
        时间
        1000ms
        内存
        128MiB
        难度
        9
        标签
        (无)
        递交数
        14
        已通过
        4
        上传者