4 条题解

  • 0
    @ 2026-5-16 11:45:50

    用stack把递归改为手动循环版本

    #include <bits/stdc++.h>
    using namespace std;
    
    struct node{
        int val;
        vector<int> child;
    };
    
    //用数组来存放树的节点,所以可以直接根据下标来访问到对应的节点
    node tree[10005];
    
    //在全局声明的变量是分配在堆内存区域,内容已经自动初始化了不担心乱码
    vector<int> ans;
    
    
    //这里是循环的视角,不是递归的视角,所以输入参数是根节点下标root而不是递归写法的任意节点u。
    void dfs(int root){
        if(root==-1){
            return;
        }
    
        //栈里是节点的下标,栈顶元素是当前正在访问的节点的下标
        stack<int> s;
        s.push(root);//栈stack相当于是FILO的待办
        while(!s.empty()){
            int u=s.top();
            s.pop();
            ans.push_back(tree[u].val);
            //当前节点的儿子节点需要入栈
            // for(int a:tree[u].child){
            //     s.push(a);
            // }
            for(int i=tree[u].child.size()-1;i>=0;i--){//因为栈是FILO,所以要逆序入栈才能保证儿子节点的访问顺序和递归写法一样
                s.push(tree[u].child[i]);
            }
        }
    }
    
    int main(){
        int n;
        cin>>n;
        if(n==0){
            return 0;
        }
        for(int i=0;i<n;i++){
            int val,k;
            cin>>val>>k;
            tree[i].val=val;
            for(int j=0;j<k;j++){
                int c;
                cin>>c;
                tree[i].child.push_back(c);
            }
        }
    
        //开始前序DFS
        dfs(0);
        for(int i=0;i<ans.size();i++){
            cout<<ans[i]<<(i<ans.size()?" ":"");
        }
        return 0;
    }
    
    
    • 0
      @ 2026-5-16 11:29:43

      递归调用版

      #include <bits/stdc++.h>
      using namespace std;
      
      struct node{
          int val;
          vector<int> child;
      };
      
      //用数组来存放树的节点,所以可以直接根据下标来访问到对应的节点
      node tree[10005];
      
      //在全局声明的变量是分配在堆内存区域,内容已经自动初始化了不担心乱码
      vector<int> ans;
      
      void dfs(int u){
          ans.push_back(tree[u].val);
          for(int a: tree[u].child){
              dfs(a);
          }
          // for(int i=0;i<tree[u].child.size();i++){
          //     dfs(tree[u].child[i]);
          // }
      }
      
      int main(){
          int n;
          cin>>n;
          if(n==0){
              return 0;
          }
          for(int i=0;i<n;i++){
              int val,k;
              cin>>val>>k;
              tree[i].val=val;
              for(int j=0;j<k;j++){
                  int c;
                  cin>>c;
                  tree[i].child.push_back(c);
              }
          }
      
          //开始前序DFS
          dfs(0);
          for(int i=0;i<ans.size();i++){
              cout<<ans[i]<<(i<ans.size()?" ":"");
          }
          return 0;
      }
      
      
      • 0
        @ 2025-12-30 14:58:30

        为了方便你在 OJ(Online Judge)上创建测试数据,我为你编写了一个完整的数据生成器。该程序会自动生成符合 NOI 竞赛要求的 1.in ~ 10.in 以及对应的 1.out ~ 10.out 文件。

        数据生成逻辑设计

        1. 覆盖范围
          • 测试点 1-2:边界情况(n=0,n=1n=0, n=1)。
          • 测试点 3-4:特殊形态(星形树、极深链形树)。
          • 测试点 5-6:完全多叉树(平衡结构)。
          • 测试点 7-10:大规模随机树(n=104n=10^4),严格控制树高 1000\le 1000
        2. 安全性
          • 使用 std::stack 进行非递归遍历生成标准答案,防止生成器自身爆栈。
          • 使用 std::mt19937 提高随机性。
          • 严格遵循 N104N \le 10^4 的限制,确保 .in 文件大小适中。

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

        #include <iostream>
        #include <fstream>
        #include <vector>
        #include <string>
        #include <random>
        #include <algorithm>
        #include <stack>
        #include <ctime>
        
        using namespace std;
        
        // 题目约束
        const int MAXN = 10000;
        const int MAXVAL = 10000;
        const int MAXHEIGHT = 1000;
        
        struct Node {
            int val;
            vector<int> children;
        };
        
        // 使用非递归方式生成前序遍历序列 (标准答案)
        vector<int> get_preorder(int n, const vector<Node>& tree) {
            vector<int> res;
            if (n == 0) return res;
            
            stack<int> s;
            s.push(0); // 根节点固定为0
            
            while (!s.empty()) {
                int u = s.top();
                s.pop();
                res.push_back(tree[u].val);
                
                // 为了前序遍历 (从左到右),栈需要反向压入子节点
                for (int i = (int)tree[u].children.size() - 1; i >= 0; --i) {
                    s.push(tree[u].children[i]);
                }
            }
            return res;
        }
        
        // 生成树结构的核心逻辑
        void generate_test_case(int case_num, int n, int type) {
            string in_name = to_string(case_num) + ".in";
            string out_name = to_string(case_num) + ".out";
            ofstream fin(in_name);
            ofstream fout(out_name);
        
            if (n == 0) {
                fin << 0 << endl;
                return;
            }
        
            vector<Node> tree(n);
            vector<int> depth(n, 0);
            mt19937 rng(time(0) + case_num);
        
            // 1. 生成节点值
            for (int i = 0; i < n; ++i) {
                tree[i].val = rng() % (MAXVAL + 1);
            }
        
            // 2. 构造树结构 (确保 0 为根)
            depth[0] = 1;
            for (int i = 1; i < n; ++i) {
                int p;
                if (type == 1) { // 星形:所有人都连根
                    p = 0;
                } else if (type == 2) { // 链形:i 连 i-1 (需受 MAXHEIGHT 约束)
                    p = (i < MAXHEIGHT) ? i - 1 : rng() % MAXHEIGHT;
                } else { // 随机树
                    // 启发式:在深度允许的范围内随机选父节点
                    do {
                        p = rng() % i;
                    } while (depth[p] >= MAXHEIGHT);
                }
                tree[p].children.push_back(i);
                depth[i] = depth[p] + 1;
            }
        
            // 3. 输出 .in 文件
            fin << n << endl;
            for (int i = 0; i < n; ++i) {
                fin << tree[i].val << " " << tree[i].children.size();
                for (int child : tree[i].children) {
                    fin << " " << child;
                }
                fin << "\n";
            }
        
            // 4. 计算并输出 .out 文件 (使用非递归前序遍历)
            vector<int> ans = get_preorder(n, tree);
            for (int i = 0; i < (int)ans.size(); ++i) {
                fout << ans[i] << (i == (int)ans.size() - 1 ? "" : " ");
            }
            fout << endl;
        
            fin.close();
            fout.close();
            cout << "Case " << case_num << " finished. n=" << n << endl;
        }
        
        int main() {
            // 1-2: 边界
            generate_test_case(1, 0, 0);
            generate_test_case(2, 1, 0);
            
            // 3-4: 特殊形态
            generate_test_case(3, 100, 1); // 星形
            generate_test_case(4, 1000, 2); // 链形
        
            // 5-6: 中等随机
            generate_test_case(5, 500, 0);
            generate_test_case(6, 2000, 0);
        
            // 7-10: 大规模 (N=10000)
            for (int i = 7; i <= 10; ++i) {
                generate_test_case(i, 10000, 0);
            }
        
            return 0;
        }
        

        数据说明与分析

        1. 时间复杂度分析 (生成器)

        • 构树过程:对于每个测试点,我们只遍历一次节点 O(N)O(N)。由于加入了 while(depth[p] >= MAXHEIGHT) 检查,在最坏随机情况下,寻找父节点的时间也接近常数(因为合法的父节点占大多数)。
        • 答案生成:使用栈进行非递归遍历,每个节点进出栈各一次,时间复杂度为 O(N)O(N)
        • 总计:生成 10 个测试点的时间复杂度为 O(N)O(\sum N),在 N=104N=10^4 的量级下,生成速度在毫秒级,不会超时。

        2. 空间复杂度分析

        • 内存占用:主要消耗在存储 vector<Node>,每个节点存其子节点编号。总边数为 N1N-1,空间复杂度为 O(N)O(N)。对于 N=104N=10^4,内存占用约几百 KB。
        • 文件大小控制
          • 每个节点在 .in 文件中大约占 10~20 字节(数值+空格)。
          • 10410^4 节点对应的单个文件约 200KB
          • 10 个测试点总大小约 2MB 左右,完全符合 OJ 系统的上传要求。

        3. 读题关键词与易错点提示 (OJ 创建者视角)

        • 节点编号:此生成器严格遵循 0 为根节点,且子节点按插入顺序(即输入顺序)决定前序遍历的“左到右”顺序。
        • 空树处理n=0 是 NOI 常见的“杀手”边界,请确保你的 OJ 判题脚本能正确处理空行。
        • 格式要求:样例输出末尾通常不建议留多余空格。此代码中通过 (i == size-1 ? "" : " ") 逻辑处理了行末空格。

        如何使用

        1. 将上述代码保存为 gen.cpp
        2. 使用 g++ gen.cpp -o gen -std=c++14 编译。
        3. 运行 ./gen,当前目录下即生成 1.in ~ 10.out
        • 0
          @ 2025-12-30 14:55:22

          在 NOI 竞赛中,处理树结构题目通常有两种主流写法:递归(DFS)迭代(手动维护栈)。对于 N104N \le 10^4 的数据规模,两者在时间效率上差异不大,但在极端情况下(如树退化成链),递归可能导致系统栈溢出,而迭代法更为稳健。

          以下是按照从基础到进阶的完整代码实现。


          版本一:基础递归 DFS 版(NOI 赛场最常用)

          思路思考: 这是最直观的解法。利用函数调用栈,先处理当前节点,再递归处理子节点。

          • 优点:代码极其简洁,逻辑清晰。
          • 缺点:如果树的深度很大(如 10510^5 以上且没有系统栈保护),可能触发 Segmentation Fault
          #include <iostream>
          #include <vector>
          #include <algorithm>
          
          using namespace std;
          
          // 常量定义,根据题目规模设定
          const int MAXN = 10005;
          
          // 定义 N 叉树结构
          struct Node {
              int val;
              vector<int> children;
          } tree[MAXN];
          
          vector<int> result;
          
          // 递归函数:前序遍历
          void dfs(int u) {
              // 1. 记录当前节点值 (根)
              result.push_back(tree[u].val);
              
              // 2. 依次递归遍历每一个子节点 (从左到右)
              for (int child_id : tree[u].children) {
                  dfs(child_id);
              }
          }
          
          int main() {
              // 加速 I/O
              ios::sync_with_stdio(false);
              cin.tie(0);
          
              int n;
              if (!(cin >> n)) return 0;
              if (n == 0) return 0;
          
              // 读入树结构
              // 假设输入格式:节点编号 0 到 n-1,每行:值 子节点数 子节点1...
              for (int i = 0; i < n; ++i) {
                  int id, k, val;
                  cin >> val >> k;
                  tree[i].val = val;
                  for (int j = 0; j < k; ++j) {
                      int child;
                      cin >> child;
                      tree[i].children.push_back(child);
                  }
              }
          
              // 从根节点(编号0)开始 DFS
              dfs(0);
          
              // 输出结果
              for (int i = 0; i < result.size(); ++i) {
                  cout << result[i] << (i == result.size() - 1 ? "" : " ");
              }
              cout << endl;
          
              return 0;
          }
          

          版本二:迭代优化版(手动模拟栈)

          思路思考: 为了解决递归深度限制问题,我们手动维护一个 std::stack

          • 难点:前序遍历顺序是“根 -> 左子 -> 右子”。但在栈中,先进入的后出来。
          • 策略:为了让左边的子节点先被弹出处理,我们需要将子节点按 从右往左 的顺序压入栈中。
          #include <iostream>
          #include <vector>
          #include <stack>
          #include <algorithm>
          
          using namespace std;
          
          const int MAXN = 10005;
          
          struct Node {
              int val;
              vector<int> children;
          } tree[MAXN];
          
          void iterativePreorder(int root) {
              if (root < 0) return;
          
              stack<int> s;
              s.push(root);
          
              vector<int> result;
              // 预分配内存,减少 vector 扩容开销
              result.reserve(MAXN); 
          
              while (!s.empty()) {
                  int u = s.top();
                  s.pop();
          
                  // 访问当前节点
                  result.push_back(tree[u].val);
          
                  // 关键点:反向压栈!
                  // 因为栈是后进先出,为了让左孩子先出栈,必须先压入右孩子
                  for (int i = (int)tree[u].children.size() - 1; i >= 0; --i) {
                      s.push(tree[u].children[i]);
                  }
              }
          
              for (int i = 0; i < result.size(); ++i) {
                  cout << result[i] << (i == result.size() - 1 ? "" : " ");
              }
              cout << endl;
          }
          
          int main() {
              // 省略与版本一相同的读入代码...
              // 此处直接调用 iterativePreorder(0);
              return 0;
          }
          

          复杂度分析思考过程

          1. 时间复杂度:O(N)O(N)

          • 分析:无论递归还是迭代,每个节点都恰好进栈/出栈一次(或被递归函数访问一次)。
          • 子节点遍历:虽然每个节点有多个子节点,但所有节点的子节点总数加起来等于 N1N-1(树的边数)。
          • 结论:总操作次数与节点数 NN 成线性关系。

          2. 空间复杂度:O(N)O(N)

          • 存储空间:邻接表 vector<int> children 存储了 N1N-1 条边,占用 O(N)O(N)
          • 辅助空间
            • 递归版:系统栈深度最坏为 O(N)O(N)
            • 迭代版:手动维护的栈 stack<int> 在最坏情况下(所有节点都是根的孩子)存储 N1N-1 个节点,占用 O(N)O(N)

          时间复杂度优化建议 (NOI 竞赛技巧)

          1. 快速 I/O: 对于 N>105N > 10^5 的题目,cin/cout 即使加了 tie(0) 也可能比 scanf/printf 慢。在 NOI 中建议使用:

            inline int read() {
                int x=0,f=1;char ch=getchar();
                while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
                while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
                return x*f;
            }
            
          2. 避免 std::vector 频繁拷贝: 在 for (int child_id : tree[u].children) 中使用引用 & 虽在处理 int 时提升不明显,但如果处理复杂对象,应养成 const auto& 的习惯。

          3. 链式前向星: 如果题目对空间限制极其苛刻(例如只有 16MB),std::vector 的内存扩容机制(通常是 1.5 到 2 倍倍增)可能会浪费空间。此时可以改用链式前向星存图:

            int head[MAXN], nxt[MAXN], to[MAXN], cnt;
            void add_edge(int u, int v) {
                to[++cnt] = v;
                nxt[cnt] = head[u];
                head[u] = cnt;
            }
            // 注意:链式前向星遍历出来的是子节点的倒序,
            // 若要严格“从左到右”,需先将子节点存下来反转或调整加边顺序。
            

          总结:读题关键词与陷阱

          • “前序遍历”:看到它就要想到 根 -> 子 的顺序。
          • “N 叉树”:意味着子节点数量不固定,代码中必须有遍历子节点列表的循环。
          • “从左到右”:这是一个非常关键的细节。如果使用实现,必须反向压栈;如果使用链式前向星,由于其“头插法”特性,遍历出的顺序默认是反向的,需要额外注意。
          • 数据规模N=104N=10^4 很小,但如果 N=106N=10^6,一定要把递归改为迭代,或者使用 std::thread 扩充系统栈,否则极易爆栈。
          • 1

          信息

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