2 条题解

  • 0
    @ 2025-12-24 9:14:21

    制作 OJ 测试数据时,构造具有弱点针对性的数据比单纯的随机数据更重要。对于“简化路径”这道题,我们需要针对:多余斜杠、根目录越界弹出、隐藏文件夹格式(如 ...)以及最大长度约束进行专项构造。

    以下是为你准备的自动化数据生成器,采用 C++14 标准编写。

    一、 数据生成器代码 (C++14)

    #include <iostream>
    #include <fstream>
    #include <vector>
    #include <string>
    #include <algorithm>
    #include <random>
    #include <ctime>
    
    using namespace std;
    
    // ================= 标准标程逻辑 (用于生成 .out) =================
    string get_standard_output(string path) {
        vector<string> stk;
        string cur;
        path += "/";
        for (char c : path) {
            if (c == '/') {
                if (cur == "..") {
                    if (!stk.empty()) stk.pop_back();
                } else if (cur != "" && cur != ".") {
                    stk.push_back(cur);
                }
                cur = "";
            } else {
                cur += c;
            }
        }
        if (stk.empty()) return "/";
        string res = "";
        for (string s : stk) res += "/" + s;
        return res;
    }
    
    // ================= 随机路径构造工具 =================
    string charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_";
    mt19937 rng(time(0));
    
    string gen_name(int len) {
        string s = "";
        for(int i=0; i<len; ++i) s += charset[rng() % charset.size()];
        return s;
    }
    
    // ================= 主生成逻辑 =================
    void write_test_case(int id, string path) {
        string in_name = to_string(id) + ".in";
        string out_name = to_string(id) + ".out";
        
        ofstream fout(in_name);
        fout << path << endl;
        fout.close();
        
        ofstream fans(out_name);
        fans << get_standard_output(path) << endl;
        fans.close();
        
        cout << "Generated Case " << id << " (Length: " << path.length() << ")" << endl;
    }
    
    int main() {
        // Case 1-4: 官方样例
        write_test_case(1, "/home/");
        write_test_case(2, "/../");
        write_test_case(3, "/home//foo/");
        write_test_case(4, "/a/./b/../../c/");
    
        // Case 5: 根目录边界与多斜杠混合
        write_test_case(5, "///////../../..////././../");
    
        // Case 6: 复杂点号测试 (Unix中 ... 是合法目录名)
        write_test_case(6, "/.../a/../b/..../c/./..");
    
        // Case 7: 深度嵌套 (测试栈的深度)
        string deep = "";
        for(int i=0; i<100; ++i) deep += "/" + gen_name(5);
        write_test_case(7, deep);
    
        // Case 8: 大量回退 (测试频繁 pop)
        string back = "/root";
        for(int i=0; i<200; ++i) back += "/dir";
        for(int i=0; i<250; ++i) back += "/.."; // 回退次数超过入栈次数
        write_test_case(8, back);
    
        // Case 9: 包含特殊字符与长目录名
        string long_dir = "/";
        for(int i=0; i<10; ++i) long_dir += gen_name(50) + "/./";
        write_test_case(9, long_dir);
    
        // Case 10: 极限长度 (L=3000) 混合随机
        string max_case = "/";
        while(max_case.length() < 2900) {
            int op = rng() % 5;
            if(op == 0) max_case += gen_name(rng()%10 + 1) + "/";
            else if(op == 1) max_case += "../";
            else if(op == 2) max_case += "./";
            else if(op == 3) max_case += "//";
            else max_case += ".../"; // 容易错写成 pop 的情况
        }
        write_test_case(10, max_case);
    
        return 0;
    }
    

    二、 测试点设计思路(创建 OJ 必读)

    作为教练,在设置 OJ 数据时,这 10 个测试点分别承担了不同的“刷人”任务:

    测试点 类型 考察重点
    1-4 基础样例 确保最基本的 Unix 逻辑实现正确。
    5 根目录越界 很多选手会在栈空时执行 pop_back() 导致非法内存访问,此点必测。
    6 迷惑性点号 考察对 ...... 的区分。注意:只有两个点才是回退。
    7 深度增长 考察 vector 动态扩容或栈空间的稳定性。
    8 负向溢出 模拟在深层目录后疯狂执行 ..,检查是否能正确停留在 /
    9 格式规范 检查输出是否满足“不能以斜杠结尾”和“多个斜杠合并”。
    10 综合压力 L=3000L=3000 长度下,各种操作高频切换,检查代码的综合鲁棒性。

    三、 考场避坑与性能优化建议

    1. 字符串拼接优化: 在 C++ 中,res = res + "/" + dir 会产生大量临时 string 对象。在生成大数据或处理极限 LL 时,建议改写为:

      res.append("/");
      res.append(dir);
      

      或者预先计算总长度并使用 reserve()

    2. 非递归声明: 本生成器和标程均采用单次循环迭代。在处理路径这种具有线性层级关系的问题时,严禁使用递归深度遍历,否则在面对 Case 7 这种上百层的目录时,容易在某些栈空间受限的评测机上触发 Segment Fault

    3. Unix 核心逻辑: 读题时务必提醒学生:路径切分后,只有 ""(空)、"."(当前)和 ".." (上层)是有特殊含义的,其他所有包含点号的组合(如 .....hidden)都视为普通目录名。这是此题最高频的错点。

    你可以直接运行此生成器,它会生成一套完美的本地测试集。加油!

    • 0
      @ 2025-12-24 9:12:27

      你好,同学。这道题是字符串处理中的经典题目,核心在于对隐含逻辑(栈)的提取。在 NOIP/NOI 竞赛中,字符串模拟题往往伴随着复杂的逻辑判断,保持代码整洁和逻辑清晰是成功的关键。

      以下是符合 C++14 标准的竞赛级参考代码。

      一、 简化路径 标准题解 (C++14)

      #include <iostream>
      #include <vector>
      #include <string>
      #include <algorithm>
      
      using namespace std;
      
      /**
       * 核心逻辑:
       * 1. 利用 "/" 作为分隔符提取每一个目录名。
       * 2. 使用 vector 模拟栈的操作:
       *    - 遇到 "..":如果栈不空,则弹出栈顶(回退到上一级)。
       *    - 遇到 "." 或 空字符串:忽略(保持当前目录)。
       *    - 遇到 正常目录名:压入栈中。
       */
      
      string simplifyPath(string path) {
          vector<string> stk;
          string current;
          path += "/"; // 易错点:在末尾强行加一个 '/',确保最后一个目录名能被处理逻辑识别
      
          for (int i = 0; i < path.length(); ++i) {
              if (path[i] == '/') {
                  // 当遇到分隔符时,处理之前累积的目录名 current
                  if (current == "..") {
                      if (!stk.empty()) {
                          stk.pop_back(); // 返回上一级目录
                      }
                  } else if (current != "" && current != ".") {
                      // 易错点:只有非空且不为 "." 的字符串才是有效目录名
                      stk.push_back(current);
                  }
                  current = ""; // 重置累积器
              } else {
                  current += path[i];
              }
          }
      
          // 结果构造
          if (stk.empty()) return "/"; // 边界情况:如果栈为空,说明在根目录
      
          string res = "";
          for (const string& dir : stk) {
              res += "/" + dir;
          }
          return res;
      }
      
      int main() {
          // 竞赛优化:加速 I/O
          ios::sync_with_stdio(false);
          cin.tie(nullptr);
      
          string path;
          if (!(cin >> path)) return 0;
      
          cout << simplifyPath(path) << endl;
      
          return 0;
      }
      

      二、 复杂度分析思考过程

      1. 时间复杂度

      • 扫描过程:我们对输入字符串 path 进行了单次线性扫描。对于长度为 LL 的字符串,扫描的时间复杂度是 O(L)O(L)
      • 字符串操作
        • 在循环中,每个字符只被处理一次。
        • 虽然 current += path[i] 和结果拼接涉及字符串拷贝,但在整个过程中,每个目录名被拷贝和移动的次数是常数级的。
      • 结论:综合时间复杂度为 O(L)O(L)。在 L=3000L=3000 的数据规模下,运算量极小,完全符合 1.0s 的时限要求。

      2. 空间复杂度

      • 存储空间:使用了 vector<string> 存储目录路径。
      • 最坏情况:如果路径是 /a/a/a/a/...,栈中需要存储所有的目录名,空间消耗与原字符串长度成线性关系。
      • 结论:空间复杂度为 O(L)O(L)

      三、 关键点与易错点总结(考场避坑指南)

      1. 末尾斜杠处理

        • 很多同学会漏掉原路径末尾的最后一个目录。
        • 技巧:在处理前给原字符串手动补一个 /(如代码第 20 行),可以统一逻辑,不需要在循环外额外处理 residual。
      2. 栈空的边界

        • 执行 .. 操作(即 pop)之前,必须先判断栈是否为空。在根目录下继续执行 .. 是合法的,但结果依然是根目录。如果直接 pop 会导致 Runtime Error
      3. 空字符串的产生

        • 连续的斜杠(如 ///)在分割后会产生空字符串。逻辑中必须使用 current != "" 来过滤掉这些无效的分割块。
      4. 结果拼接

        • 注意规范路径的要求。如果最后栈内没有元素,应该返回 / 而不是空串。

      四、 时间复杂度优化建议

      虽然 O(L)O(L) 已经是理论最优复杂度,但在实际竞赛中,如果你追求极致的常数优化(例如针对 L=106L=10^6 的极限数据),可以考虑以下方案:

      • 减少临时对象: 在代码中,current += path[i] 会产生很多细小的内存分配。在 C++ 中,可以改用双指针记录 startend 的位置,直接在原字符串上操作,只在需要 push_back 时才构造一次 string
      • 预分配空间: 由于路径长度已知,可以对 stk 使用 reserve() 函数,减少 vector 动态扩容带来的开销。
      • 原生数组替代 vector: 如果内存限制极严,可以使用字符数组 char[] 手动维护栈空间,虽然实现起来复杂一些,但常数项会更小。

      这道题是考察**“将需求转化为数据结构操作”**的优良练习。希望你能通过此题,建立起“遇到层级关系,首选栈结构”的思维模式。加油!

      • 1

      信息

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