3 条题解

  • 0
    @ 2025-12-17 16:33:18

    关于为什么第一个版本代码会有个WA,而第二个版本可以AC?

    这两个代码虽然核心算法思路都是滑动窗口,且时间复杂度都是 O(N)O(N),但导致你遇到 WA(特别是 Test #1 的 Read (EOF), expect 0 错误)以及潜在 Runtime Error 的原因主要有两点:对空输入的处理逻辑字符类型的隐式转换

    核心原因 1:对输入流状态的判断导致“零输出”

    这是导致 Test #1 报错 Read (EOF), expect 0 的直接原因。

    你的代码 (Code A):

    // 致命行
    if (!getline(cin, s)) return 0; 
    
    • 分析:你把 getline 放在了 if 条件里。如果测试数据文件是完全空的(0字节)(这也是很多生成器生成的空串默认行为),getline 读取时会直接遇到文件结束符(EOF),返回 false
    • 后果:程序直接进入 return 0;退出了,没有执行任何 cout 语句
    • 评测机视角:我调用了你的程序,还没等到你输出任何东西,你就结束了。我期待输出 0,但读到了 EOF。\rightarrow WA

    AC代码 (Code B):

    getline(cin, s);
    // ... 后续逻辑 ...
    cout << ans << endl;
    
    • 分析:Code B 只是执行 getline,不判断返回值(或者说不因 EOF 而提前退出)。
    • 后果:即使文件是空的,getline 读完后 s 是空字符串 ""。程序继续向下执行,n=0,循环不执行,最后稳稳地执行 cout << 0 << endl;
    • 评测机视角:输出了 0\rightarrow AC

    潜在原因 2:数组下标越界风险 (Signed char 陷阱)

    这是一个在 NOIP/OI 中极易丢分且难以调试的隐蔽 Bug。

    你的代码 (Code A):

    char char_in = s[right]; // char 在 C++ 中通常是有符号的 (-128 到 127)
    freq[char_in]++;         // 危险!如果 char_in 是负数,freq 访问越界
    
    • 分析:C++ 标准中,char 类型通常是 signed char。如果测试数据中包含 ASCII 码 > 127 的字符(比如乱码、扩展字符),或者你的生成器不小心生成了负数 char,那么 char_in 会是一个负整数(例如 -60)。
    • 后果freq[-60] 访问非法内存。这通常会导致 Runtime Error (RE/SegFault),在某些特定内存布局下甚至可能导致 WA。
    • 修复:必须强制转换为 unsigned char 或者 int 再做下标。

    AC代码 (Code B):

    unsigned char ch = s[R]; // 强制转为无符号 (0 到 255)
    // ... pos[ch] ...       // 安全
    
    • 分析:显式使用 unsigned char,确保下标永远在 0-255 之间。这也是为什么数组 pos 开了 256 而不是 128 的原因,为了兜底所有单字节字符。

    总结对比表

    特性 你的代码 (Code A) AC代码 (Code B) 结果差异
    空文件处理 if (!getline) return;
    直接退出,无输出
    getline(cin, s);
    继续运行,输出 0
    Code A 在 Test #1 报错 "Expect 0"
    数组下标 使用 char (可能为负数) 使用 unsigned char (0-255) Code A 遇特殊字符会崩溃 (RE)
    窗口收缩 while 循环一步步收缩 (慢一些) 记录下标直接 Jump (更快) 两者都是 O(N),但 Code B 常数更小
    数组大小 128 (不够扩展ASCII) 256 (安全覆盖) Code B 更稳健

    修改建议

    把你的代码改成下面这样,就能通过了:

    int main() {
        ios::sync_with_stdio(false);
        cin.tie(nullptr);
    
        string s;
        getline(cin, s); // 1. 去掉 if 判断,强制往下走
    
        int n = s.length();
        // 2. 如果之前是空文件,s为空,n为0,这里自然处理
        if (n == 0) {
            cout << 0 << endl;
            return 0;
        }
    
        // 3. 数组开大一点,防止特殊数据
        vector<int> freq(256, 0); 
        
        // ...
        for (int right = 0; right < n; ++right) {
            // 4. 强制转无符号,防止下标为负
            unsigned char char_in = s[right]; 
            freq[char_in]++; 
            
            while (freq[char_in] > 1) {
                unsigned char char_out = s[left]; // 同样转无符号
                freq[char_out]--; 
                left++;           
            }
            max_len = max(max_len, right - left + 1);
        }
        // ...
    }
    
    • 0
      @ 2025-12-17 16:19:40

      你好!作为一名经验丰富的OI教练,我为你准备了一份高质量的数据生成器。这份代码不仅集成了经过验证的AC解法(Solver),还精心设计了10个测试点,覆盖了从边界情况、特殊陷阱到大数据压测的所有场景。

      这份生成器会直接在当前目录下生成 1.in/1.out10.in/10.out 文件,完全符合OJ数据录入格式。

      数据生成策略设计

      为了保证测试数据的全面性,我设计了以下分布:

      • Test 1-2 (边界极值): 空串、单字符。考察程序是否会在空输入下崩溃。
      • Test 3-4 (逻辑特例): 全不重复(答案为N)、全重复(答案为1)。
      • Test 5-6 (经典陷阱): "abba"(回退陷阱)、"pwwkew"(中间重复)。这是最容易WA的地方。
      • Test 7 (特殊字符): 包含空格、符号、数字混合。考察 int pos[256] 是否越界。
      • Test 8 (大数据-稀疏): 长度 5×1045 \times 10^4,全随机ASCII。
      • Test 9 (大数据-密集): 长度 5×1045 \times 10^4,只包含 'a'-'c'。考察频繁冲突下的滑动窗口性能。
      • Test 10 (大数据-周期): 长度 5×1045 \times 10^4,长周期重复。

      C++ 数据生成器完整代码

      请保存为 generator.cpp,编译运行即可。

      /**
       * 题目:无重复字符的最长子串 - 数据生成器
       * 功能:生成 1.in/1.out ~ 10.in/10.out
       * 环境:C++14 Standard
       * 作者:OI Gold Medal Coach
       */
      
      #include <iostream>
      #include <fstream>
      #include <string>
      #include <vector>
      #include <algorithm>
      #include <random>
      #include <cstring>
      #include <ctime>
      
      using namespace std;
      
      // ==========================================
      // Part 1: 标准 AC 解法 (Solver)
      // ==========================================
      // 这一部分用于生成 .out 标准答案
      class Solver {
      public:
          int lengthOfLongestSubstring(string s) {
              if (s.empty()) return 0;
              
              // 使用 256 大小覆盖所有 ASCII 字符(含扩展)
              // pos[ch] 记录字符 ch 上次出现的下标
              int pos[256];
              // 快速初始化为 -1
              memset(pos, -1, sizeof(pos));
              
              int n = s.size();
              int ans = 0;
              int L = 0; // 左边界
              
              for (int R = 0; R < n; ++R) {
                  unsigned char ch = (unsigned char)s[R]; // 强转 unsigned 防止下标为负
                  
                  // 核心逻辑:防止 L 回退
                  if (pos[ch] != -1) {
                      L = max(L, pos[ch] + 1);
                  }
                  
                  pos[ch] = R;
                  ans = max(ans, R - L + 1);
              }
              return ans;
          }
      };
      
      // ==========================================
      // Part 2: 数据生成工具箱
      // ==========================================
      
      // 随机数引擎
      mt19937 rng(time(0));
      
      // 生成指定长度的随机字符串
      // type 0: 全 ASCII 可见字符 (32-126)
      // type 1: 仅小写字母
      // type 2: 极小字符集 (a, b, c) 用于制造高频冲突
      string generate_random_string(int len, int type) {
          string res = "";
          uniform_int_distribution<int> dist_ascii(32, 126); // 空格到波浪号
          uniform_int_distribution<int> dist_lower('a', 'z');
          uniform_int_distribution<int> dist_small('a', 'c');
          
          for (int i = 0; i < len; ++i) {
              char ch;
              if (type == 0) ch = (char)dist_ascii(rng);
              else if (type == 1) ch = (char)dist_lower(rng);
              else ch = (char)dist_small(rng);
              res += ch;
          }
          return res;
      }
      
      // 生成全部重复的字符串
      string generate_repeat_string(int len, char c) {
          return string(len, c);
      }
      
      // 生成由不重复片段循环组成的字符串 (e.g., abcdeabcde...)
      string generate_periodic_string(int len, int period_len) {
          string res = "";
          string pattern = "";
          for(int i=0; i<period_len; ++i) pattern += (char)('a' + (i % 26));
          
          while(res.length() < len) {
              res += pattern;
          }
          return res.substr(0, len);
      }
      
      // ==========================================
      // Part 3: 主程序
      // ==========================================
      int main() {
          Solver solver;
          
          // 循环生成 10 个测试点
          for (int i = 1; i <= 10; ++i) {
              string in_file_name = to_string(i) + ".in";
              string out_file_name = to_string(i) + ".out";
              
              ofstream fin(in_file_name);
              ofstream fout(out_file_name);
              
              string input_data = "";
              string desc = ""; // 仅用于控制台输出提示
      
              // ---- 针对不同测试点设计数据 ----
              switch(i) {
                  case 1: 
                      // 边界:空串
                      input_data = ""; 
                      desc = "Empty String";
                      break;
                  case 2: 
                      // 边界:单字符
                      input_data = "x"; 
                      desc = "Single Char";
                      break;
                  case 3: 
                      // 特例:全不重复 (小写字母)
                      input_data = "abcdefghijklmnopqrstuvwxyz"; 
                      desc = "All Unique (Small)";
                      break;
                  case 4: 
                      // 特例:全重复
                      input_data = generate_repeat_string(100, 'a'); 
                      desc = "All Repeated";
                      break;
                  case 5: 
                      // 陷阱:回退测试 "abba"
                      input_data = "abba"; 
                      desc = "Trap: abba";
                      break;
                  case 6: 
                      // 混合逻辑
                      input_data = "abcabcbb"; 
                      desc = "Sample: abcabcbb";
                      break;
                  case 7: 
                      // 中等规模 + 特殊字符 (空格、符号)
                      input_data = generate_random_string(2000, 0); 
                      desc = "Medium Random (Full ASCII)";
                      break;
                  case 8: 
                      // 大数据:全随机 ASCII
                      // 长度 50000 (题目上限)
                      input_data = generate_random_string(50000, 0); 
                      desc = "Max Random (Full ASCII)";
                      break;
                  case 9: 
                      // 大数据:高频冲突 (只有 a,b,c)
                      // 考察滑动窗口频繁伸缩的性能
                      input_data = generate_random_string(50000, 2); 
                      desc = "Max Random (Small Charset)";
                      break;
                  case 10:
                      // 大数据:周期性长串
                      // 考察跨越周期的判断
                      input_data = generate_periodic_string(50000, 26); 
                      desc = "Max Periodic";
                      break;
              }
      
              // ---- 写入 .in 文件 ----
              // 注意:OJ 题目通常是单行字符串,或者通过 getline 读取
              fin << input_data; 
              
              // ---- 计算并写入 .out 文件 ----
              int ans = solver.lengthOfLongestSubstring(input_data);
              fout << ans << endl; // 一般输出带换行符比较规范
      
              // ---- 关闭文件流 ----
              fin.close();
              fout.close();
      
              cout << "Generated Test Case " << i << ": " << desc << " [Len: " << input_data.length() << "]" << endl;
          }
      
          cout << "\nAll 10 test cases generated successfully!" << endl;
          return 0;
      }
      

      使用指南

      1. 编译与运行: 将代码保存为 gen.cpp,在命令行执行:

        g++ gen.cpp -o gen -O2
        ./gen
        

        (Windows下直接运行 gen.exe)

      2. 生成结果: 你会在当前文件夹下看到 20 个文件:

        • 1.in ~ 10.in:输入数据
        • 1.out ~ 10.out:对应的标准答案
      3. OJ配置提示

        • 时间限制:建议设置为 1000ms (C++代码对于 50000 长度几乎是瞬时的,通常只需 10-20ms)。
        • 内存限制:建议 64MB 或 128MB。
        • 输入格式:因为题目输入可能包含空格(Test Case 7 和 8),如果你用来判题的代码使用 cin >> s 会导致读取不全,必须确保你的判题机或测试代码使用 getline(cin, s)

      时间和空间复杂度复盘

      • 生成器效率:主要耗时在生成长字符串。长度最大 5×1045 \times 10^4,生成+求解 10 组数据总时间通常在 0.1秒以内,远不会超时。
      • 栈溢出风险:生成器和 Solver 均未使用递归,且 pos 数组仅 256 大小(栈上分配约 1KB),不会爆栈。
      • 大数据强度:Test 8, 9, 10 达到了题目数据范围的上限(50000),且分别覆盖了字符集稀疏(一般情况)、密集(最坏情况)和周期性特征,能有效卡掉暴力解法(O(N2)O(N^2) 在 50000 下约为 2.5×1092.5 \times 10^9 次运算,必然 TLE)。
      • 0
        @ 2025-12-17 15:54:10

        好的,我是你的OI竞赛教练。

        这道《无重复字符的最 long 子串》是滑动窗口思想的另一座高峰。与固定窗口不同,这里的窗口大小是动态变化的,需要我们精确地控制窗口的“扩张”与“收缩”,是考察对算法理解深度的经典好题。

        一、 标准题解代码 (C++14)

        这份代码使用了最经典、最易于理解的“不定长滑动窗口”模型。

        这个版本的case1会WA

        /*
         * 题目:无重复字符的最长子串 (Longest Substring Without Repeating Characters)
         * 算法:滑动窗口 (Sliding Window / 双指针) + 哈希计数
         * 时间复杂度:O(N),其中 N 为字符串长度
         * 空间复杂度:O(Σ),Σ 为字符集大小,本题为 O(128) 即 O(1)
         * 编写标准:NOIP/CSP C++14
         */
        
        #include <iostream>
        #include <string>
        #include <vector>
        #include <algorithm> // for max()
        
        using namespace std;
        
        int main() {
            // 【关键点1】加速I/O
            ios::sync_with_stdio(false);
            cin.tie(nullptr);
        
            string s;
            if (!getline(cin, s)) return 0; // 使用 getline 读取可能包含空格的整行
        
            int n = s.length();
            if (n == 0) {
                cout << 0 << endl;
                return 0;
            }
        
            // 哈希计数数组,题目提示字符集为 ASCII,范围 0-127
            vector<int> freq(128, 0); 
            
            int left = 0;      // 窗口左指针
            int max_len = 0;   // 全局最长长度
        
            // 右指针 right 遍历整个字符串
            for (int right = 0; right < n; ++right) {
                char char_in = s[right];
                freq[char_in]++; // 新字符进入窗口
        
                // 【关键点2】收缩窗口的条件
                // 当新进入的字符在窗口内的计数 > 1 时,说明出现了重复
                // 必须从左侧收缩窗口,直到重复问题解决
                while (freq[char_in] > 1) {
                    char char_out = s[left];
                    freq[char_out]--; // 左侧字符离开窗口
                    left++;           // 左指针右移
                }
        
                // 【易错点1】更新答案的位置
                // 只有在 while 循环结束后,当前窗口 [left, right] 才是一个合法的无重复子串
                // 此时我们用它的长度来更新全局最大值
                max_len = max(max_len, right - left + 1);
            }
        
            cout << max_len << endl;
        
            return 0;
        }
        

        这个版本可以AC

        /**
         * 题目:无重复字符的最长子串
         * 风格:NOIP/CSP 竞赛标准 C++14
         * 作者:OI Gold Medal Coach
         */
        
        #include <iostream>
        #include <string>
        #include <vector>
        #include <algorithm>
        #include <cstring> // 用于 memset
        
        using namespace std;
        
        // 定义常量,方便维护
        // ASCII 码表通常为 0-127,扩展 ASCII 到 255。
        // 开 256 大小足以覆盖所有单字节字符,无需 map。
        const int MAX_CHAR = 256; 
        
        int main() {
            // ==========================================
            // 1. IO 读写优化 (竞赛必备)
            // ==========================================
            // 关闭 C++ 标准流与 C 标准流的同步,加快读写速度
            ios::sync_with_stdio(false);
            cin.tie(NULL);
        
            // ==========================================
            // 2. 数据读入
            // ==========================================
            string s;
            // 注意:题目中的子串可能包含空格,不能用 cin >> s;
            // 必须使用 getline 读取整行
            getline(cin, s);
        
            // ==========================================
            // 3. 算法核心:滑动窗口 + 数组模拟哈希
            // ==========================================
            
            // pos[c] 存储字符 c 上一次出现的下标
            // 使用静态数组或局部数组均可,局部数组记得初始化
            int pos[MAX_CHAR];
            
            // 初始化为 -1,表示所有字符都还没出现过
            memset(pos, -1, sizeof(pos));
        
            int n = s.length();
            int ans = 0; // 记录最长长度
            int L = 0;   // 滑动窗口的左边界 (Left)
        
            // R (Right) 为滑动窗口的右边界,主动向右扫描
            for (int R = 0; R < n; ++R) {
                // 【易错点1】char 类型在某些环境下是有符号的 (-128 到 127)
                // 如果输入特殊符号,直接做下标会越界崩溃。
                // 必须强制转换为 unsigned char。
                unsigned char ch = s[R];
        
                // 检查重复:如果当前字符之前出现过 (pos[ch] != -1)
                if (pos[ch] != -1) {
                    // 【易错点2】核心逻辑 - 左边界的更新
                    // 只有当上一次出现的位置 pos[ch] 在当前窗口内 (>= L) 时,
                    // 它是阻碍我们扩张的“绊脚石”,L 需要跳过它 (pos[ch] + 1)。
                    // 必须取 max,防止 L 倒退(即“回退陷阱”)。
                    L = max(L, pos[ch] + 1);
                }
        
                // 更新状态:记录当前字符 ch 最新的位置是 R
                pos[ch] = R;
        
                // 计算当前合法窗口长度,尝试更新全局答案
                // 窗口区间为 [L, R],长度 = R - L + 1
                ans = max(ans, R - L + 1);
            }
        
            // ==========================================
            // 4. 结果输出
            // ==========================================
            cout << ans << endl;
        
            return 0;
        }
        

        二、 复杂度分析思考过程

        1. 时间复杂度分析

        • 思考过程
          1. 代码的核心是两个指针:leftright
          2. 外层 for 循环驱动 right 指针从 0 移动到 n-1,总共移动了 nn 次。
          3. 内层 while 循环驱动 left 指针移动。虽然它在 for 循环内部,但需要分析 left 指针的总移动次数
          4. left 指针从 0 开始,只向右移动,并且永远不会超过 right。在整个算法的生命周期中,left 指针最多也只会从 0 移动到 n-1
          5. 因此,right 指针和 left 指针都各自对字符串进行了一次线性扫描。每个字符最多被 right 访问一次,被 left 访问一次。
        • 结论:总的操作次数与 nn 成正比,时间复杂度为 O(N)O(N)。这比 O(N2)O(N^2) 的暴力枚举要高效得多。

        2. 空间复杂度分析

        • 思考过程
          • 我们使用了哪些额外的存储空间?
          • 主要是 freq 数组。
          • 这个数组的大小是 128,它的大小取决于字符集的大小 (Σ\Sigma),而不是输入字符串 s 的长度 N
        • 结论:空间复杂度是 O(Σ)O(\Sigma)。由于 ASCII 字符集大小是固定的常数,所以空间复杂度为 O(1)O(1)

        三、 时间复杂度优化建议

        上述解法已经是 O(N)O(N) 的最优时间复杂度,但我们可以通过一种更巧妙的方式来移动 left 指针,从而省去 while 循环,进行常数级别的优化。

        优化思路:使用哈希表记录字符的最新位置 我们可以用一个哈希表(或数组)last_pos[128] 来存储每个字符最后一次出现的下标。

        right 指针遇到一个字符 s[right] 时:

        1. 我们检查 s[right] 上一次出现的位置 last_pos[s[right]]
        2. 如果这个位置在当前窗口的左边界 left 之后或重合(即 last_pos[s[right]] >= left),说明 s[right] 在当前窗口内构成了重复。
        3. 此时,我们不需要用 while 循环慢慢移动 left,而是可以直接将 left 跳跃到重复字符的下一个位置,即 left = last_pos[s[right]] + 1。这保证了新的窗口 [left, right] 中不再包含旧的那个 s[right]
        4. 如果 last_pos[s[right]]left 之前,说明它不在当前窗口内,不构成重复,left 不动。
        5. 每次循环后,更新 last_pos[s[right]] = right

        优化后的核心逻辑 (Snippet):

        // 仅供参考的优化版逻辑
        vector<int> last_pos(128, -1); // 初始化为-1,表示从未出现过
        int left = 0;
        int max_len = 0;
        
        for (int right = 0; right < n; ++right) {
            char char_in = s[right];
            
            // 如果该字符出现过,并且其上次出现位置在当前窗口内
            if (last_pos[char_in] >= left) {
                // 直接将左指针跳到重复字符的下一个位置
                left = last_pos[char_in] + 1;
            }
            
            // 更新该字符的最新位置
            last_pos[char_in] = right;
            
            // 更新最大长度
            max_len = max(max_len, right - left + 1);
        }
        

        教练建议: 这两种解法的时间复杂度量级都是 O(N)O(N)。第一种 while 循环收缩窗口 的方法是滑动窗口的“标准型”,更通用,易于理解和扩展到其他问题。第二种 left 指针跳跃 的方法是针对本题“无重复”特性的一种巧妙优化,代码更简洁。在竞赛中,两种方法都必须掌握。初学者建议先熟练掌握第一种。

        • 1

        信息

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