NOIP极限DDL冲刺计划-语言关-Phase0

准高二零基础学 OI 第一周:7 天,43 个文件,31 题 AC,33 条错误模式

我马上高二,会 Python,初中摸过 FPGA,高一自学了组成原理和操作系统,C++ 一行没写过,目标 NOIP 2026 省二。8 月 10 日到 17 日,7 天啃完 OI 语言关,代价是 43 个题文件、31 题 AC、33 条错误模式。

带我的教练是个 AI——opencode 命令行工具,跑 deepseek 模型,被我当 OI 教练使。我有组成原理底子,直接看汇编理解;做题则要求每题过本地 judge 再交洛谷。

这 7 天我干了什么

节奏固定:教练先讲概念,再做题,每题本地 judge 验证、洛谷提交、WA 就记一条错误模式。

Day 1 到 Day 7 依次是 IO、分支循环、数组字符串、函数递归、STL 两批加排序、综合模拟。毕业考是 NOIP2016 提高组 T1(P1563),独立 AC 算过关。做完计划任务还能开”超额池”提前刷后面的题,7 天下来 43 个题文件、31 题 AC 。

有两天很挫败:P1308 五轮 WA(下面细说),P1055 也卡了很久。但这周最大的收获全在 WA 里。

33 条错误模式全量清单

Day 2 起每次 WA 都登记一条错误模式,7 天记了 33 条。按心智模型分七组,全部列出来,当给自己做周复盘,也给读者当参考。

组 1:C++ 语法陷阱(6 条)

  1. ^ 是异或不是乘方(P5717)。我写边长平方直接 ^2(之前明明记得cpp乘方是^),死活找不到 bug。C++ 没有 **^ 优先级还低于 +,$a \text{ XOR } 2$ 和 $a^2$ 完全不是一回事。平方就老老实实 a*a。这是 Python 直觉污染。
  2. 运算符优先级(P5711/P5710,两次中招):<<?:&& 混用必须加括号。cout << x ? 1 : 0 实际是 (cout << x) ? 1 : 0,三目的结果根本没输出。编译器的 -Wparentheses 警告是免费的定位器。
  3. 未初始化自引用(P5710):bool a = (a % 2 == 0)——右边的 a 用的是自己,未初始化。-Wuninitialized 警告出现必查。
  4. 输出分隔符(P5715):元素之间打空格,不是每个元素后打。for 循环里 cout << a[i] << ' ' 会留下行尾空格。
  5. 抄写错误(P5717):三角形判定 (c+b<c) 抄成对不上的比较对象。写完通读一遍条件行。
  6. struct 漏分号 + 成员函数漏括号(P5742):struct Student{...} 结尾没分号是编译错;s.is_excellent 不带括号是函数指针,恒为真——编译能过、逻辑全错、样例碰巧对,最危险的一种。

组 2:边界与定义(5 条)

  1. 边界语义抠字眼(P5711):闰年是”能被 4 整除且不能被 100 整除,或能被 400 整除”,不是只有 %4
  2. 临界值手动推(P5713):条件写 n<5,应为 n<=5——n=5 时本地 25 分钟 < 洛谷 26 分钟,恰好是临界点。样例过了不等于边界过了。
  3. 下标基线对齐(P5729):数组按 0..w-1 初始化,切割坐标却是 1..w,边界层整个漏判。自测要构造”切掉坐标 w 整层”这种用例。
  4. 筛法漏 1(P5736):线性筛从 2 开始标记,is_comp[1] 永远是 false,输入含 1 就会把 1 当质数输出。
  5. “位置”默认指字符下标(P1308):词序号 ≠ 字符位置。样例里 to 恰好是第一个词(词序号 0 = 字符位置 0),两种定义输出相同,只有洛谷隐藏数据能区分。样例区分不了定义时,构造”能区分定义的用例”(如 no it is notis,期望 1 6)再提交。

组 3:输入输出形态(5 条)

  1. 输出格式读全题面(P1055):ISBN 判错时要输出完整正确的 ISBN(0-670-82162-4),不是只补校验位(4)。本地样例只覆盖 Right 分支,错误分支的格式必须自己补测。
  2. 特殊值单独处理(P1055):校验位 'X' 代表 10,'X' - '0' 等于 40 会静默出错。读题时标注所有特殊输入值。
  3. 大数必须用 string(P1781):票数可达 100 位,long long 只有 19 位,读进去就溢成垃圾值。字符串比大小先比长度、同长再比字典序——"9" > "100" 是陷阱。数据范围决定存储类型。
  4. “独立单词”≠”空格分隔”(P1308):cin >> word 按空格分词,文章里 to, 会被粘成 "to,"。正确做法是 isalpha 清洗非字母为空格再分词,”单词 = 连续字母段”。
  5. getline 的 CRLF 刺客(P1308):洛谷数据是 \r\n,本地是 \ngetline 读进来的字符串末尾带 \r,比较永远失败。防御:if (!s.empty() && s.back()=='\r') s.pop_back();。这个坑我教练自己都栽过。

组 4:容器操作(4 条)

  1. 弹栈/出队前判空(P1739):输入 )( 时空栈 pop() 是未定义行为 = RE。pop() 无返回值,且对空容器调用是 UB。
  2. 解析题的结束标记最先判断(P1449):后缀表达式的 @ 被当运算符弹栈,弹两个数但栈里只有一个,空栈 RE。结束符是控制字符不是运算数,遍历时必须第一个处理。
  3. front/back 混淆(P1540):清缓存标记用 back(),但 pop() 弹的是 front()——清除端和弹出端不一致,标记错位,样例碰巧掩盖。写队列代码时问自己:pop 前访问的端和 pop 的端一致吗?
  4. “满了才挤”是先判满再入(P1540):cnt >= m 在入队后才判断,第 m 个词一进来就被挤掉,内存等效 m-1 容量。正确:入队前 size() >= m 先弹再入。

组 5:类型(1 条)

  1. 阶乘/累乘必用 long long(P5739):$20! \approx 2.43 \times 10^{18}$,int 上限才 $2^{31}-1 \approx 2.1 \times 10^9$。看到阶乘、排列组合、累乘,第一反应 long long。基准条件写 n <= 1fact(0)

组 6:循环与控制流(4 条)

  1. if 不带花括号只保护一行(P1739 二次 WA):if (empty) cout<<...; return 0;return 0 不在 if 内,第一个 ) 就无条件退出。铁律:所有 if/for/while 一律带花括号。-Wmisleading-indentation 警告出现必查。
  2. break vs continue(线性筛):把 break 写成 continue,i % p == 0 后继续用更大的质数筛,每个合数被多次标记,操作数实测从 $1.6n$ 涨到 $3.1n$。break 是”到此为止”,continue 是”跳过本次”。
  3. 循环边界 <= 陷阱(线性筛):j <= cnt 访问到未写入的 primes[cnt](初值 0),i % 0 除零。我当时的认知错误是把 cnt 当成 size()-1(最后元素下标),实际上 cnt 等价于 vector.size()(元素个数/下一个空位)——遍历用 j < cnt
  4. 读字符循环:读取放循环开头(P1042 死循环):continue 跳过循环末尾的 cin.get(c)c 永远是 '\n' 永远不等于 'E'。铁律:while (cin.get(c)) 读在开头,任何 continue/break 都不会弄丢下一次读取。

组 7:下标与取模(3 条)

  1. 下标运算不要莫名 -1(P1563):((pos+step)%n) - 1 在取模结果为零时变成 -1 越界。我当时的想法是”vector 从 0 开始而小人从 1 开始”,但这是混淆了编号偏移量:编号转下标才 -1(第 k 个元素 = 下标 k-1),移动距离不 -1(数 k 个 = 走 k 步)。P1563 里只有后者。
  2. 大步数防负取模(P1563):pos - step + n 在 step > n 时仍为负(C++ 负数取模结果是负的,$(-3) \bmod 7 = -3$ 不是 4)。正确写法是 $((pos – step) \bmod n + n) \bmod n$,两次取模保证非负。
  3. 方向规则用题面例子反推(P1563):”朝内+左=顺时针”是直觉,但题面例子说 singer(朝内)左数第 3 个是 archer——0 号左数 3 到 4 号,是逆时针。写完方向判断后,拿题面例子对一步验证,1 分钟就能发现方向反了。

顺带:环形 = 模算术(P1563 最大的收获)

P1563 我一开始完全没想到取模。想到的是环形链表——然后被教练一句点醒:环形结构的数学标准表示就是模算术,时钟就是活例子。

3 点加 11 小时是 2 点($14 \bmod 12 = 2$),你从来不会觉得时钟难,因为它”转一圈回到原点”。玩具小人围成的圈和时钟是同一个数学对象:n 个位置 = 模 n,走 s 步 = 加 s(模 n)。链表是”实现环”的一种方式,取模是”计算环上位置”的方式——题目要的是后者。

pos = (pos + s) % n;              // 顺时针走 s 步
pos = ((pos - s) % n + n) % n;    // 逆时针走 s 步(两次 % 防负)

顺带一提,这个取模做法是 O(1) 的:一次指令直接算到位,不用一步一步走。所以它既解决”环形怎么表示”,也解决”步数大(s 可达 10^9)会不会超时”——链表逐格走早就 TLE 了。

组 8:其他(5 条)

  1. 变量遮蔽(线性筛):函数内 int cnt 遮蔽全局 cnt,填表和验证用两个变量,编译还不报错(-Wshadow 能查)。函数内不声明与全局同名的变量。
  2. 逐字符读只过滤 \n 是半个修复(P1042):cin.get 逐字符读时 \r 是独立字符,只 if (c=='\n') continue 会让 \r 落到 else 分支被记分。铁律:用白名单写法 if (c != 'W' && c != 'L') continue;——只接受合法字符,其余全跳过。
  3. “结束”语义按题面规则而非物理量(P1042):乒乓球一局结束 = 一方 ≥ 11 且分差 ≥ 2,不是”打了 11 个球”。循环结束后的残余状态也要输出(最后一局,含 0:0)。
  4. 打满不等于获胜:$11:10$ 还要继续打,领先 2 分才赢。
  5. 样例不全时分支没测全(P1055/P1540 通用):本地 AC 只证明”我测过的情况”正确。构造边界用例(n=5、坐标 w 整层、CRLF、全 0)自测,比提交洛谷试错快。

再顺带:补码和取模是同一个东西

然后我又想到:补码的本质就是模运算。在 32 位 int 里,$-3$ 的补码是 0xFFFFFFFD = $4294967293 = 2^{32} – 3$。你看,这不就是 $(-3) \bmod 2^{32}$ 的非负余数吗?负数在计算机里的表示,就是这个数对 $2^{32}$ 取模后的同余类

所以:

  • int 溢出回绕($2^{31}-1 + 1 = -2^{31}$)不是 bug,是模 $2^{32}$ 算术的正常结果
  • 为什么 C++ 里 (-3) % 7 = -3 而不是 4?因为 C++ 取模向零截断,返回的是”余数”,不是”非负同余代表”。数学上 -3 和 4 在模 7 下是同一个东西,只是 C++ 选了个负数来表示
  • 所以防负取模 ((pos - step) % n + n) % n 不是 hack,是在手动找回数学意义上的非负余数——那个真正代表”环上的位置”的数

环形取模、补码、溢出回绕,这三件事在数学上是同一个对象:模 $2^k$ 或模 $n$ 的同余类。想通这点,看 int 回绕就像看时钟转圈一样自然。

顺带:线性筛的思路(Day 4 自研)

Day 4 学筛质数,我要求先自己想,别看书。朴素做法是埃氏筛:每遇到一个质数 p,就把它的倍数全标记。问题在于重复标记——12 被 2 筛一次、被 3 筛一次,做了无用功,复杂度 $O(n \log \log n)$。

线性筛的突破口是个数学事实(算术基本定理):每个合数有唯一的质因数分解,所以有唯一的最小质因子。那么:

让每个合数 $x$ 只被它的最小质因子筛掉一次。每个合数只操作一次 → 总复杂度 $O(n)$。

代码的灵魂在那一行 break

for (int i = 2; i <= n; i++) {
    if (!is_comp[i]) primes[cnt++] = i;
    for (int j = 0; j < cnt; j++) {
        int p = primes[j];
        if (i * p > n) break;         // 防越界
        is_comp[i * p] = 1;           // 标记
        if (i % p == 0) break;        // ★ 灵魂:p 已经是 i 的最小质因子,再筛就重复了
    }
}

那句 i % p == 0 一旦成立,就说明 p 整除 i,那么后面更大的质数 $p’$ 筛出来的 $i \cdot p’$,它的最小质因子还是 p(因为 p 已经在 i 里了)——所以 $i \cdot p’$ 应该由更小的数负责筛,我在这里继续就是重复劳动。

自研的时候我把它写成 continue,实测操作数从 $1.6n$ 涨到 $3.1n$——每个合数被标记了不止一次,线性性质就没了。break 是”到此为止”,continue 是”跳过本次”,这一字之差就是 $O(n)$ 和 $O(n \log \log n)$ 的区别。

六组心智模型

33 条错误模式背后是六组心智模型。C++ 和 Python 的差别不在语法,在心智。

  1. 值语义 vs 引用语义。Python 传引用,C++ 默认按值拷贝。函数参数、遍历要不要 &,debug 一半的 bug 在这。
  2. 没有动态大整数。int 上限约 21 亿,溢出是静默的。看到阶乘、累乘、1e9 级别直接 long long。
  3. 区间思想。STL 全是左闭右开 $[begin, end)$,循环边界默认 <,除非闭区间语义明确。
  4. 编译与运行分离 + O2g++ -O2 会”优化”未定义行为,本地对、评测炸。
  5. 题面是唯一真理n<5n<=5 的区别,就是样例过了和 AC 的区别。
  6. 暴力对拍。逻辑手推不可靠时,写 Python 朴素解随机对拍,几分钟抓出所有隐藏 bug。

读题清单五问

这周三次 WA(P5713、P5729、P1055)全是一个原因:题没读完。于是定了一张清单,每题动笔前过一遍:

  1. 数据范围:n 最大多少?决定算法和类型。
  2. 输入格式:下标从 0 还是 1?多个数怎么隔开?
  3. 输出格式:行尾要不要空格?错误分支输出什么?
  4. 特殊值:’X’、-1、0、空串?单独处理。
  5. 边界条件:n=1、全 0、最大值时表现如何?题面保证了什么?

本地 AC 不等于题面读完,这条花了不少代价才明白。

我的训练环境

先交代硬件软件,因为这一周能这么快,环境帮了大忙。

  • 系统:Fedora 43,Python 3.14,g++ 15(编译命令 g++ -O2 -Wall -Wextra -std=c++17,和 NOIP 环境一致)
  • 编辑器:Neovim 0.11 + lazy.nvim 插件管理,装了 treesitter(C/C++ 高亮)、clangd LSP(写代码时实时报错和补全)、nvim-cmp(补全)、telescope(文件搜索)。clangd 是最值的——变量名打错、类型不匹配,还没编译就标红,等于多了一道免费的类型检查
  • 本地评测脚本 judge.sh:一条命令完成”编译 → 跑样例 → 比对 → 计时”,输出 AC/WA 和耗时,不用手动敲 g++ 和 diff
  • 样例自动抓取:用 curl 带 cookie 抓洛谷题目页,解析页面里内嵌的 JSON("samples":[[输入,输出],...]),自动落到 lang/samples/<题号>/ 目录。做题前数据已经备好,验证就是一条命令
  • 目录结构lang/ 放语言关练习,algo/ 放专题,notes/ 放复盘,每个文件命名 P题号.cpp,模板统一从 tools/template.cpp 复制(bits/stdc++.h + ios 关同步)

我原来以为 OI 训练就是”洛谷网页上写代码提交”,后来发现本地闭环才是主战场:样例验证、对拍、复盘全在本地做,洛谷只用来交最终答案。环境建好后,一天能多挤出一两个小时的做题时间。

三件工具,让训练效率翻倍

  1. 本地评测脚本g++ -O2 -Wall -std=c++17 编译 + 样例比对 + 计时,一条命令完成”本地验证”闭环。先把样例数据抓下来(curl + cookie 解析洛谷页面里的 JSON),每题本地 judge 过了再交洛谷。
  2. 暴力对拍:怀疑有隐藏 bug 时,写一个 Python 朴素参考解(直接模拟题意,不做任何优化)+ 随机数据生成器,几百上千组对比。P1540 两轮 WA 人工测试全放过,对拍 2000 组抓出 77 组失败。这是”本地 AC 但洛谷 WA”的根治手段。
  3. 错误模式清单:每次 WA 登记一条,按心智模型分组。它不只是事后补救,更是下次写代码时提前激活的检查点——P1563 开写前我主动想到 (int)size() 强转(P5727 教训),没等踩坑先预防了。教练管这个叫”正向记录”。

除了踩坑,还有两件高兴的事

第一件是上面说的主动预防。

第二件是复盘出了方法。以前学东西”懂了就完”,现在每次 WA 都问”这暴露了我哪个思维漏洞”。33 条错误模式里几乎没有一条属于智商问题,全是习惯和细节。

另外,教练在 P5741 之后陪我做了均摊复杂度分析:我直觉”排序+窗口”能到 O(n log n),实测最坏情况(全部同分)下窗口永不提前结束,还是 O(n²)。”看着快”不等于”量级变了”,要看最坏情况的总和有没有界。这比 AC 本身值钱。

下周开始 Phase 1:枚举、贪心、前缀和、二分、DFS/BFS、并查集,8 月底前吃完核心。数学我倒不怵,要练的是把套路变肌肉记忆,还有”先写暴力拿部分分”的纪律。

活着回来的话,再写一篇 Phase 1 复盘。

(co-work with deepseek-v4-flash-0731)

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇