3 条题解
-
0
关于为什么第一个版本代码会有个WA,而第二个版本可以AC?
这两个代码虽然核心算法思路都是滑动窗口,且时间复杂度都是 ,但导致你遇到 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。 WA。
AC代码 (Code B):
getline(cin, s); // ... 后续逻辑 ... cout << ans << endl;- 分析:Code B 只是执行
getline,不判断返回值(或者说不因 EOF 而提前退出)。 - 后果:即使文件是空的,
getline读完后s是空字符串""。程序继续向下执行,n=0,循环不执行,最后稳稳地执行cout << 0 << endl;。 - 评测机视角:输出了
0。 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);
继续运行,输出 0Code 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
你好!作为一名经验丰富的OI教练,我为你准备了一份高质量的数据生成器。这份代码不仅集成了经过验证的AC解法(Solver),还精心设计了10个测试点,覆盖了从边界情况、特殊陷阱到大数据压测的所有场景。
这份生成器会直接在当前目录下生成
1.in/1.out到10.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 (大数据-稀疏): 长度 ,全随机ASCII。
- Test 9 (大数据-密集): 长度 ,只包含 'a'-'c'。考察频繁冲突下的滑动窗口性能。
- Test 10 (大数据-周期): 长度 ,长周期重复。
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; }使用指南
-
编译与运行: 将代码保存为
gen.cpp,在命令行执行:g++ gen.cpp -o gen -O2 ./gen(Windows下直接运行
gen.exe) -
生成结果: 你会在当前文件夹下看到 20 个文件:
1.in~10.in:输入数据1.out~10.out:对应的标准答案
-
OJ配置提示:
- 时间限制:建议设置为 1000ms (C++代码对于 50000 长度几乎是瞬时的,通常只需 10-20ms)。
- 内存限制:建议 64MB 或 128MB。
- 输入格式:因为题目输入可能包含空格(Test Case 7 和 8),如果你用来判题的代码使用
cin >> s会导致读取不全,必须确保你的判题机或测试代码使用getline(cin, s)。
时间和空间复杂度复盘
- 生成器效率:主要耗时在生成长字符串。长度最大 ,生成+求解 10 组数据总时间通常在 0.1秒以内,远不会超时。
- 栈溢出风险:生成器和 Solver 均未使用递归,且
pos数组仅 256 大小(栈上分配约 1KB),不会爆栈。 - 大数据强度:Test 8, 9, 10 达到了题目数据范围的上限(50000),且分别覆盖了字符集稀疏(一般情况)、密集(最坏情况)和周期性特征,能有效卡掉暴力解法( 在 50000 下约为 次运算,必然 TLE)。
-
0
好的,我是你的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. 时间复杂度分析
- 思考过程:
- 代码的核心是两个指针:
left和right。 - 外层
for循环驱动right指针从0移动到n-1,总共移动了 次。 - 内层
while循环驱动left指针移动。虽然它在for循环内部,但需要分析left指针的总移动次数。 left指针从0开始,只向右移动,并且永远不会超过right。在整个算法的生命周期中,left指针最多也只会从0移动到n-1。- 因此,
right指针和left指针都各自对字符串进行了一次线性扫描。每个字符最多被right访问一次,被left访问一次。
- 代码的核心是两个指针:
- 结论:总的操作次数与 成正比,时间复杂度为 。这比 的暴力枚举要高效得多。
2. 空间复杂度分析
- 思考过程:
- 我们使用了哪些额外的存储空间?
- 主要是
freq数组。 - 这个数组的大小是
128,它的大小取决于字符集的大小 (),而不是输入字符串s的长度N。
- 结论:空间复杂度是 。由于 ASCII 字符集大小是固定的常数,所以空间复杂度为 。
三、 时间复杂度优化建议
上述解法已经是 的最优时间复杂度,但我们可以通过一种更巧妙的方式来移动
left指针,从而省去while循环,进行常数级别的优化。优化思路:使用哈希表记录字符的最新位置 我们可以用一个哈希表(或数组)
last_pos[128]来存储每个字符最后一次出现的下标。当
right指针遇到一个字符s[right]时:- 我们检查
s[right]上一次出现的位置last_pos[s[right]]。 - 如果这个位置在当前窗口的左边界
left之后或重合(即last_pos[s[right]] >= left),说明s[right]在当前窗口内构成了重复。 - 此时,我们不需要用
while循环慢慢移动left,而是可以直接将left跳跃到重复字符的下一个位置,即left = last_pos[s[right]] + 1。这保证了新的窗口[left, right]中不再包含旧的那个s[right]。 - 如果
last_pos[s[right]]在left之前,说明它不在当前窗口内,不构成重复,left不动。 - 每次循环后,更新
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); }教练建议: 这两种解法的时间复杂度量级都是 。第一种
while循环收缩窗口 的方法是滑动窗口的“标准型”,更通用,易于理解和扩展到其他问题。第二种left指针跳跃 的方法是针对本题“无重复”特性的一种巧妙优化,代码更简洁。在竞赛中,两种方法都必须掌握。初学者建议先熟练掌握第一种。 - 思考过程:
- 1
信息
- ID
- 19348
- 时间
- 1000ms
- 内存
- 128MiB
- 难度
- 10
- 标签
- (无)
- 递交数
- 6
- 已通过
- 1
- 上传者