按 OJ 题目顺序学习 · 边做边更新 配套人:DSH 助手 | 学习方式:做一题 → 讲透 → 记录
一、学习路线总览
C 语言的主干就这几块,OJ 的题目顺序基本就是照这个来的:
| 阶段 | 核心内容 | 对应题型 | 状态 |
|---|---|---|---|
| ① 顺序结构 | 变量、类型、scanf/printf、算术运算 | 绝对值、复利、位数 | ✅ 基本掌握 |
| ② 选择结构 | if/else if/else、&&/||、switch | 闰年、分段函数、四则运算 | ✅ 基本掌握 |
| ③ 循环结构 | while、for、累加/计数/打擂台 | 求和、求最大值 | 🔄 正在学 |
| ④ 嵌套循环 | 循环里套循环 | 图形打印、九九表、质数 | ⬜ 未开始 |
| ⑤ 数组 | 一维/二维数组、排序、查找 | 查表、统计、矩阵 | ⬜ 未开始 |
| ⑥ 字符与字符串 | char、字符数组、string.h | 字符统计、回文、加密 | 🔄 部分(char 学过) |
| ⑦ 函数 | 定义、调用、参数、递归 | 阶乘、斐波那契 | ⬜ 未开始 |
| ⑧ 指针 | 地址、指针运算、数组与指针 | —— | ⬜ 未开始 |
| ⑨ 结构体 | struct、结构数组 | 学生成绩管理 | ⬜ 未开始 |
| ⑩ 文件 | fopen/fclose/读写 | 文件统计 | ⬜ 未开始 |
当前重点:③ 循环结构 → 开始接触「经典算法」(第 38 题辗转相除法是第一个)。
二、核心代码模式速查
这五个骨架覆盖了 80% 的入门题。看到题目先想”往哪个骨架里套”。
模式 1:分段判断(选择结构)
if (条件1) { ...}else if (条件2) { /* ← 必须是 else if,不能是独立 if */ ...}else { /* 兜底 */ ...}要点:分支互斥时一定用 else if 链。
模式 2:累加器(求和/求平均)
long long sum = 0; /* ① 先清零 */for (i = 0; i < n; i++) { scanf("%d", &x); sum += x; /* ② 累加 */}printf("%lld\n", sum); /* ③ 输出 */要点:sum 必须清零;防溢出用 long long。
模式 3:计数器(数个数)
int cnt = 0; /* 先清零 */for (...) { if (满足条件) cnt++; /* 命中才 +1 */}模式 4:打擂台(求最值)
int max = 第一个数; /* ① 立擂主,不能用 0! */for (...) { scanf("%d", &x); if (x > max) max = x; /* ② 严格 > ,平手不换 */}要点:
- 初值不能给 0(除非确定有非负数)
- 用严格
>→ 平手时保留先出现的
模式 5:判断某性质(试除/逐位处理)
/* 例:判断质数 */int isPrime = 1;for (i = 2; i * i <= n; i++) { /* 试到开方就够 */ if (n % i == 0) { isPrime = 0; break; }}
/* 例:逐位处理(水仙花数、求位数) */while (n > 0) { int d = n % 10; /* 取最后一位 */ ... n /= 10; /* 砍掉最后一位 */}三、⭐ 个人易错点清单(基于实际踩过的坑)
这份清单比上面的模式更值钱 —— 每一条都是你真犯过的错。
A. 标点符号类(最高频!)
| 错误 | 例子 | 后遗症 |
|---|---|---|
| 漏分号 | printf(...) 后没写 ; | 编译错误 |
| 多分号 | if (x>0); | if 被空语句吃掉,else 找不到 if |
case 后写分号 | case '+'; | 必须是冒号 case '+': |
{ } 不配对 | 少一个 },后面的 if 嵌进去了 | 编译错误或逻辑全乱 |
break 位置 | default: 写到 switch 的 } 外面 | default 和 break 双报错 |
自查方法:提交前数一遍 { 和 };检查每个 if (...)、while (...) 右括号后有没有多余分号。
B. 分支结构类
| 错误 | 正确做法 |
|---|---|
该用 else if 却写了独立 if | 分支互斥 → 必须 else if |
条件写成 'a'<=c<='z' | 必须写 c>='a' && c<='z' |
if 和 else 之间插了语句 | else 必须紧跟 if 的语句 |
用了 a > b > c | 必须写 a > b && b > c |
&& / || 用混 | 都要满足 → &&;满足其一 → || |
C. 循环结构类
| 错误 | 正确做法 |
|---|---|
忘写 i++ | 死循环(TLE) |
| 差一错误 | 起点 0 配 <,起点 1 配 <= |
| 累加器没清零 | sum = 0 必须先写 |
| 打擂台初值给 0 | 全负数时输出 0;应先读第一个数当擂主 |
while (...); 多分号 | 同 if 的坑 |
D. 变量与类型类
| 错误 | 说明 |
|---|---|
| 变量未初始化 | int m; 直接用 → 输出垃圾值(实测见过 32759) |
累加用 int | 和可能超 21 亿 → 用 long long + %lld |
a*a 溢出 | 大数相乘会变负数 → 用 long long |
| 格式符不匹配 | int→%d,long long→%lld,double→%lf(scanf)/%f(printf) |
abs vs fabs | 整数用 abs(stdlib.h),小数用 fabs(math.h) |
E. 输出格式类(吃过亏!)
| 坑 | 例子 |
|---|---|
| 大小写 | 题目要 Yes/No,写了 yes/no → WA |
| 末尾标点 | 题目要 yes.(带点),漏了点 → WA |
| 保留位数 | %.2f 不是 %.6f |
| 单独占一行 | 别忘 \n |
铁律:题目要求输出什么,就一个字符不差地照抄。
F. 数学/业务理解类
| 坑 | 例子 |
|---|---|
| 分段累进漏档位 | 工资题:第三档要加上前两档拿满的钱 |
浮点数不能用 == 比 0 | 用 |s2| < 1e-10(或两边夹 -1e-10 < s2 && s2 < 1e-10) |
| 先检查再计算 | 除法前查除数;三角形先查三边关系 |
四、提交前 30 秒自查清单
- 编译一遍,看到零警告才交
- 每个
printf/scanf/break后有;吗 if (...)、while (...)、case xxx:后面没有多余分号吗{和}数量相等吗- 变量都赋初值了吗(尤其是累加器/计数器/擂主)
- 输出格式照抄题面(大小写、标点、换行、小数位)
- 在分界点各测一个值(如
x=-2、x=3、50000/50001) - 会不会溢出(
int加起来够大吗)
五、已完成的题目记录
| # | 题目 | 考点 | 当时的问题 |
|---|---|---|---|
| 1 | 求绝对值 | 顺序结构 + if | scanf 没检查返回值 |
| 2 | 存款复利 | pow vs 循环 | 用了单利公式 |
| 3 | 求位数 | / % 拆位 | 讨论了 log10 的精度陷阱 |
| 4 | 字符大小写 | char、ASCII 差 32 | 分界线写成 90、加减方向反了 |
| 5 | 字母序号 | 字符相减 | 多了个分号 |
| 6 | 三字符最大 | 打擂台 | > 方向写反(变成求最小) |
| 7 | 字符分类 | else if 链 | 漏 3 个分号 + scanf 多了空格 |
| 8 | 水仙花数 | 拆位 + 立方 | ✅ 一次通过 |
| 9 | 闰年 | &&/|| 组合 | 输出大小写 No 写成 no |
| 10 | 门票计费 | 简单分支 | 变量名 piao/n 混用 |
| 11 | 判断三角形 | 三边关系 | 多一个条件少一个条件 |
| 12 | 直角三角形 | 勾股定理 + || | 讨论了 int 溢出 |
| 13 | 象限判断 | 互斥分支 | 前两个 if 没写 else if |
| 14 | 销售工资 | 分段累进 | 比率小 10 倍 + 漏档位 |
| 15 | 分段函数 | 绝对值 | printf 插在 if/else 之间 |
| 16 | 月份天数 | 查表 vs switch | 把”闰年”当成”输出 29” |
| 17 | 四则运算 | switch + 浮点判零 | 漏除零检查;case 后写分号;default 在 } 外 |
| 18 | 绝对值最大 | 打擂台 + 平手规则 | m 未初始化 + 比错对象 |
| 19 | n 个数求和 | 循环入门 | ✅ 一次通过 |
| 20 | n 个数最大值 | 打擂台 + 循环 | 初值给 0(全负数时输出 0) |
| 21 | 不及格率 | 计数器 + 浮点除法 | 分数线写成 90;整数除法 cnt/n |
| 22 | 统计正/负/零 | 多个计数器 | i 未声明;for 分隔符写成逗号;忘读 n |
| 23 | 奇数乘积 | 累乘器(初值 1) | %n 应为 %d;%llf 应为 %lld |
| 24 | 自然对数表 | 格式宽度 + log | ✅ 一次通过 |
| 25 | 阶乘表 | 递推(fact *= i) | 内外层同用 i → 死循环;公式写成 (j-1)*j |
| 26 | 奇偶平方/立方和 | 分支累加 | 行尾写逗号;ji*ji*ji+ji;&m &n 少逗号 |
| 27 | 阶乘累加(1!+…+n!) | 递推 + 累加 | 忘读 n;内层乘到 n 而非 i;jie=0 |
| 28 | 数列平方根 | 递推 + 累加 | x=item 写在 scanf 前(item 还是 0) |
| 29 | a+aa+aaa… | 递推 + 累加 | 两行顺序反了:先 sum+=x 后 x=x*10+a |
| 30 | 猴子吃桃(倒推) | 逆推(倒着算) | 循环 n 次应为 n-1 次 + 初值 0 应为 1(两个错叠在一起) |
| 31 | 斐波那契兔子 | 二阶递推 / 滚动变量 | fib=sum 抹掉前一项 → 变成 ×2 输出 2 的幂 |
| 32 | 幸运数字(7的倍数非4的倍数) | 不枚举 + 从上界往下退 | for 条件方向反了(n<=m 永不成立);break 插在 if/else 之间;printf("%d ",n) 空格代替换行 |
| 33 | 判断素数 | 试除法 + 标志变量 flag | ①break 写在 if 外面(循环只跑一轮 → 退化成判奇偶,1000 个里错 333 个)②改完漏了 n < 2(1 被判成素数) |
| 34 | 阶乘不等式 1!+…+m! < n | while + 三条更新 + 类型溢出 | ✅ 一次通过!(前四题连踩坑后,第一次完全自己写对) |
| 35 | 学生成绩最高分(读到负数为止) | 哨兵值 + 不定项输入 + 打擂台 | ✅ 又一次性通过!(只有一处小瑕疵:printf 末尾没加 \n) |
| 36 | 逆序输出正整数各位 | % / 拆位 + 次数未知的循环 | ✅ 逻辑与输出全对(逐字节比对样例通过);唯一隐患:第 10 行 int yu=0; 写在 scanf 之后(声明与语句混写),老判题机按 C90 会报编译错误 |
| 37 | 正序输出正整数各位 | 位权 h + 两次循环(高位优先) | ✅ 通过(穷举 200 万个数、严格 C89 编译全部通过)。但连错三轮:①wei 当成”位数”用(wei++)而不是”位权”(wei*=10)②int m=n; 写在 scanf 之前(备份到的是 0)③漏掉 n = n % wei;(吐出的是前缀 1 12 123 1234 12345) |
| 38 | 最大公约数(辗转相除法) | 第一个”有名字的算法” | ✅ 一次通过! 三行核心 y = m % n; m = n; n = y; 顺序完全正确,零警告,严格 C89 编译通过。实测 400 万对穷举 + 200 万对随机大数 + 9 万对枚举对照 + 42 对相邻斐波那契 + 20 万组倍数性质 → 不一致 0 处 |
| 39 | 最大公约数 + 最小公倍数 | 先除后乘防”中间过程溢出” | ⚠️ 差一行。第一版 c = a*b/m; → 1000000 1000000 输出 1000000 -727(a*b 在 int 里先溢出)。改 c = a/m*b; 后全对。备份 a=m; b=n; 这一步他自己想到了,做对了 |
| 40 | 字符加密(凯撒挪 1 位 + 统一小写) | 环形取模 %26 + 两步变形 | 讲完待交。核心 c = 'a' + (c - 'a' + 1) % 26;必须先把大写转小写再移位(否则 ‘Z’ 变 ‘z’ 没绕回 ‘a’);scanf("%c") 不能加前导空格(空格本身要保留) |
规律:逻辑几乎没错过,错误集中在标点、边界、格式。
近期趋势(2026-10-21 晚,热身题 7-4 / 7-5):出现了一类新错误 —— “指错对象”。7-5 四个空全错,但没有一个是”不会 C”:该用循环变量 h 用了人数 w、该加人数 w 加了 1、该跟最大值比跟了前一个桶(还越界)、该写 17 写了 18。语法全对,指称全错。 对策见「六点五」的抓手二(拼音变量名 + 变量角色表)。
近期趋势(第 19~24 题):逻辑零失误,错误只剩语法/格式符 —— 说明循环骨架已掌握。
近期趋势(第 25~29 题):五题里有四题错在”递推”上 —— 不是不懂,是”递推语句的位置/顺序”出错。
近期趋势(第 30~32 题):从”写错”升级到”想错” —— 连续三题出现新骨架(倒推、二阶递推、不枚举)。
近期趋势(第 33~34 题):第 33 题把老大难”if 不加 {}”又犯了一次(break 跑到 if 外面);第 34 题一次通过 —— 说明”边界 + 类型”的意识开始建立。
近期趋势(第 34~35 题):连续两题一次通过。第 35 题两个致命坑(scanf 忘在循环里、printf 写在循环内)全部躲过 —— while 的骨架已经稳了。
近期趋势(第 36 题):连续三题一次通过。% 与 / 配对拆位、“printf 在循环里会累积输出”都是自己想通的。
近期趋势(第 37 题):前三题的”一次通过”停下了 —— 一题连错三轮。但这三轮的错法很有信息量:都不是”不懂解法”,而是「变量的身份搞错」(位数当成位权)、「变量的时机搞错」(备份写在 scanf 前)、「少写一句」(漏 n = n % wei)。这三类正是拆位题的全部雷区,一轮踩完。位权 h 的算法本身(while (n/wei>=10)) 他第一版就写对了。
近期趋势(第 38 题):第一次接触”有名字的算法”(辗转相除法),一次通过。而且这题是三行滚动变量赋值顺序最容易翻车的地方(写反就变 18),他没踩。从”拼循环”跨到”用算法”的第一步走得很稳。
★★ 两题的”最大值”对比:次数知不知道,决定用 for 还是 while
| 第 20 题 | 第 35 题 | |
|---|---|---|
| 有几个数? | 题目先给 n | 不知道,读到负数为止 |
| 用哪个循环 | for | while |
| 循环条件 | i < n(数到第 n 个) | x >= 0(读到哨兵) |
| 读数据在哪 | 循环体开头 | 循环体末尾(读下一个) |
| 擂主初值 | 先读第一个数 | 先读第一个数(一样) |
| 输出在哪 | 循环外 | 循环外(一样) |
哨兵值(结束标志)
哨兵值 = 一个特殊的数,它不是数据本身,只负责站在队尾喊”到此为止”。
7 6 5 9 2 -1 ↑ ↑ 真成绩 ────────────── 真成绩 哨兵(不是成绩!)循环条件问的是”手上这个数还是有效数据吗”(x >= 0),不是”是不是哨兵”(x != -1)。
后者只在哨兵恰好是 -1 时才对 —— 万一哨兵是 -5 就漏了。
为什么不需要专门排除哨兵: 读到 -1 时,while (x >= 0) 立刻不成立,循环体一次都不执行,哨兵根本没机会参加打擂台。
不定项输入的标准摆法(推荐背下来):
scanf("%d", &x); /* 起点:先读一个 */max = x; /* 让它先当擂主 */while (x >= 0) { /* 条件:手上这个还是有效数据吗 */ if (x > max) max = x; scanf("%d", &x); /* 更新:读下一个 ← ★ 最容易忘的一句 */}printf("%d\n", max); /* 输出:循环【外面】 */两种”输入结束”的区别:
| 结束方式 | 判断写法 | 例子 |
|---|---|---|
| 哨兵值(题面给的一个特殊数) | x >= 0 / x != 0 | 第 35 题(负数表示结束) |
| 读到文件结尾(EOF) | while (scanf("%d", &x) == 1) | 题面说”多组数据直到文件结束” |
紧凑写法(把 scanf 塞进条件):
int x, max = -1; /* 成绩非负,-1 比任何成绩都小 */while (scanf("%d", &x) == 1 && x >= 0) if (x > max) max = x;printf("%d\n", max);顺序不能换:scanf(...) == 1 必须在 && 前面(先确认读到了,再看值)。
★ 正序拆位:位权 h 三件套(第 37 题)
第 36 题是个位优先(逆序),第 37 题是最高位优先(正序)。方向反了,做法就变了。
| 第 36 题(逆序) | 第 37 题(正序) | |
|---|---|---|
| 先拿哪一位 | 个位 | 最高位 |
| 拿一位 | n % 10 | n / h |
| 削一位 | n = n / 10 | n = n % h |
| 位权/除数 | 固定 10 | 从最高位开始,每轮 h = h / 10 |
| 循环条件 | n > 0 | h > 0 |
位权 = 这一位代表多少个 1。 12345 的位权依次是 1 / 10 / 100 / 1000 / 10000。
第一步:找最高位的位权。
int h = 1;while (n / h >= 10) /* 问:n/h 还是两位数以上吗?是就再升一级 */ h = h * 10;⚠️ 必须写 n / h >= 10,不能写 n / h > 0 —— 后者会多乘一次 10,位权多一个 0,输出前面多一个 0。
⚠️ 也别写 h * 10 <= n —— 本题范围(n ≤ 10^9)内能过,但 n 接近 int 上限时 h * 10 会溢出绕圈,最后 h 变成 0 → n / h 除以零,程序直接崩溃(退出码 -1073741676 = 0xC0000094)。能用除法判断就别用乘法。
第二步:从高位到低位逐位吐。
while (h > 0) { printf("%d ", n / h); /* ① 吐出最高位 */ n = n % h; /* ② ★ 把最高位削掉(漏了就会吐"前缀")*/ h = h / 10; /* ③ 位权降一级 */}②和③是一对,缺一不可:
- 少了
n = n % h→ 吐出来的是越来越长的前缀(1 12 123 1234 12345) - 少了
h = h / 10→ 死循环,一直吐同一个数字
为什么少了②会变成前缀:n / h 的含义是”把低 h 位砍掉,留下高位部分”。不把刚吐出的那一位削掉,下一轮除数小 10 倍,就多露出前面一位。
记忆法:想「留下」最高位用 /(取商),想「扔掉」最高位用 %(取余)。
⚠️ 拆位会把 n 改掉。 循环结束后 n 已经变成 0,原值丢了。如果后面还要用 n,必须在拆位之前先备份 int m = n; —— 而且这句必须写在 scanf 之后!
实测对照(缺口那一行 vs 补上):
| 输入 | 漏 n = n % h | 补上后 |
|---|---|---|
12345 | 1 12 123 1234 12345 | 1 2 3 4 5 ✔ |
999 | 9 99 999 | 9 9 9 ✔ |
100 | 1 10 100 | 1 0 0 ✔ |
7 | 7 | 7 ✔(一位数碰巧对) |
复发错误榜(按次数排序,提交前必查)
| 错误 | 次数 | 出现题目 |
|---|---|---|
if 不加 {},或 if / else 之间夹了别的语句 | 4 | 第 15 题(printf 插在 if/else if 间)、第 32 题(break 插在 if/else 间)、第 33 题(break 在 if 外,只跑一轮)← 头号大坑 |
输出格式(大小写、末尾标点、空格 vs 换行、缺 \n) | 4 | 第 9 题 No/no、第 12 题 yes.、第 32 题 "%d "、第 33 题 printf("Yes") 缺 \n |
| 缺分号 / 多分号 / 分号写成逗号 | 6+ | 第 5、7、21、26、32 题 |
循环次数差一(n vs n-1,<= vs <) | 2 | 第 30 题猴子吃桃、第 32 题循环条件方向 |
| 漏掉边界情况(题目”注意:“里写的) | 4 | 第 7 题大小写、第 26 题除零、第 32 题范围、第 33 题 n=1 |
类型溢出(该用 long long 用了 int) | 3 | 第 30 题猴子吃桃、第 31 题斐波那契、第 34 题阶乘 |
| “备份/初始化”语句写在了数据到来之前 | 2 | 第 28 题(x = item; 在 scanf 之前)、第 37 题(int m = n; 在 scanf 之前)← 新上榜,专坑拆位/递推题 |
| “位数”与”位权”混淆 | 1 | 第 37 题(wei 当计数器用 wei++,得到 5 而不是 10000) |
| 拆位时漏掉”削位”那一句 | 1 | 第 37 题(漏 n = n % h,吐出前缀 1 12 123 1234 12345) |
★ 铁律:题目里出现”注意:…”,就当它是一行必须写的代码
第 33 题题面写着 “注意:1不是素数”,他代码里没有这一行 → 1000 个测试点错 1 个 → 报错。
第 34 题题面写着 “提示:注意计算过程中的类型溢出问题” → 他按要求用了 long long → 一次通过。
第 34 题题面写着 “提示:注意计算过程中的类型溢出问题” → 他按要求用了 long long → 一次通过。
第 37 题题面写着 “提示:注意整数运算避免使用 double 类型的函数如 pow()“,并且直接把算法给了出来 → 他没写 pow,用整数循环找位权,while (n/wei>=10) 第一版就写对了。
而且第 37 题他是自己主动说出”这次竟然还有提示”的 —— 说明这条铁律已经养成。
题目不会白写废话。凡是”注意""提示""保证""规定”后面的内容,都要落到代码里。
★ 写赋值语句前先问一句:“右边的量,现在有值了吗?”
同一个错误犯了两次(第 28 题、第 37 题),必须单列出来。
| 题 | 错的写法 | 后果 |
|---|---|---|
| 第 28 题 | x = item; 写在 scanf("%d %d", &item, &n); 之前 | item 还是 0,整题输出 0.00 |
| 第 37 题 | int m = n; 写在 scanf("%d", &n); 之前 | m 备份到的是 0,0 % wei 恒为 0,输出 0 0 0 0 |
规则:凡是”把某个变量备份/初始化到另一个变量”,那一行必须写在”数据读进来之后”。
拆位/递推题的三个雷区(第 37 题一轮全踩):
| 雷区 | 表现 |
|---|---|
| 变量的「身份」搞错 | wei 到底是”位数”(有几个数字)还是”位权”(这一位代表多少)?12345 的位数是 5,位权是 10000 |
| 变量的「时机」搞错 | 备份语句写在数据到来之前 |
| 「少写一句」 | n = n % h 漏了 → 吐出前缀;h = h / 10 漏了 → 死循环 |
递推三连问(每次写 递推 + 累加 都要问)
- 我这一步用的是”上一项”吗?(阶乘漏了
*i、写成(jiecheng-1)*jiecheng) - 递推语句放在”加”的前面还是后面?配得上变量的初值吗?(第 29 题两行反了)
- 变量初值对吗?(
fact=1不是 0;term=0不是a;x=item要等scanf之后)
三个必须区分的”递推家族”
| 家族 | 式子 | 需要几个变量 | 例题 |
|---|---|---|---|
| 一阶递推 | 新 = 旧 与 i 运算 | 1 个 | 阶乘、a+aa+aaa、平方根数列 |
| 二阶递推 | 新 = 前1项 + 前2项 | 2 个(滚动) | 斐波那契 |
| 逆推 | 由后一项反解前一项,倒着算 | 1 个 | 猴子吃桃 |
二阶递推的固定写法(滚动变量 / 滑动窗口):
int a = 1, b = 1; /* 窗口最初盖住第1、2项 */for (i = 1; i < n; i++) { /* 滚 n-1 次 */ int t = a + b; /* 先算新项存到临时变量 */ a = b; /* a 右移 */ b = t; /* b 右移 */}printf("%d\n", a);关键:a = b; b = t; 必须借助临时变量 t,绝不能写成 a = b; b = a + b;(前一项被覆盖)。
逆推题的固定三问:
- 起点是什么?(已知的最后一项)
- 翻几次?(“已知末项求首项”→ 几乎总是
n-1次) - 怎么翻?(把题目条件反解成公式)
while 还是 for?—— 看”次数知不知道”
| 用哪个 | 什么时候用 | 三零件怎么摆 |
|---|---|---|
for | 次数事先知道(数着数跑:1~n、n 个数、n-1 次) | 三零件挤在一行:for (i=1; i<=n; i++) |
while | 次数事先不知道(跑到某个条件成立为止) | 起点写在外面,条件写在括号里,更新写在循环体内 |
两者完全等价:for (A; B; C) D 就是 A; while (B) { D; C; }。
while 的头号死循环陷阱 —— 忘记写更新语句:
while (k >= m) { if (k % 4 != 0) { printf("%d\n", k); return; } /* 忘了 k -= 7; -> k 永远不变,条件永远成立 -> 死循环(OJ 报 TLE)*/}规则:更新语句要放在循环体里”每条路径都会经过”的位置,不要藏进 if 里面。
情况二:更新语句不止一条(第 34 题)
while (sum + fact < n) { sum += fact; /* 少这条 -> sum 不变 -> 死循环 */ m++; /* 少这条 -> m 不变 -> 答案永远是 0 */ fact *= (m + 1); /* 少这条 -> fact 不变 -> 死循环 */}三条更新少任何一条,不是死循环就是答案错。 写完对着数一遍:是不是三条都写了?
为什么这种题不用 for:for 的第三格只装得下一个式子,三条得挤成逗号表达式很难读;而且”循环几次”事先不知道,本来就该用 while。
★「先问后加」比「先加后撤」好
第 34 题的精髓 —— while 的条件只”问”不”改”,是试探:
while (sum + fact < n) { /* 问:加进去还成立吗? 只算,不写回任何变量 */ sum += fact; /* 答"成立"才真的加 */}对比”先加后撤”的写法(要多一句 mm-- 回退):
while (1) { mm++; f *= mm; if (s + f >= n) { mm--; break; } /* ★ 得撤销刚才那一步 */ s += f;}多一步回退,就多一类可能忘写的地方。能”先问后加”就别”先加后撤”。
类型溢出速查(什么时候必须 long long)
| 危险值 | 值 | 说明 |
|---|---|---|
int 上限 | 2147483647 | 约 21 亿 |
12! | 479001600 | 安全 |
13! | 6227020800 | 超 int 上限 → 阶乘表、阶乘不等式必须 long long |
S(13)=1!+…+13! | 6749977113 | 超 int → 阶乘累加必须 long long |
F(44) | 701408733 | 斐波那契安全 |
F(47) | 2971215073 | 超 int 上限 → 所以斐波那契题卡在 n≤44 |
| 猴子吃桃 n=30 | 1610612734 | 安全;但中间过程会达到 3221225470 超界 |
溢出的表现:结果变成负数,或者”条件永远成立”造成死循环。 判断口诀:题目给了范围,就把范围上限代进去,手算一下最大值。
另一个信号:题目里写”注意计算过程中的类型溢出问题” → 直接上 long long。
★★ 最阴的一种溢出:结果没超,中间过程超了(第 39 题)
题目说”输入保证最终结果在 int 范围内”—— 这句话只保证结果,不保证过程。
第 39 题:n, m ≤ 1000000,求最大公约数和最小公倍数。
最小公倍数 = n * m / gcd
题目保证【最小公倍数】≤ int 上限 ✔ 但没人保证【n * m】不超 —— 它最大能到 1000000 × 1000000 = 10^12 int 上限是 2147483647(约 2.1×10^9) → n * m 最大是 int 上限的 466 倍 ●现场实测(n = m = 1000000):
gcd = 1000000,正确答案 = 1000000 (确实在 int 范围内 ✔)
真实的 n*m = 1000000000000 当成 int 看 = -727379968 ← 已经变成负数了
写法1 n * m / gcd = -727 ★错★ 写法2 n / gcd * m = 1000000 ✔最小的能触发的输入:n = m = 46341 —— 46341 × 46341 = 2147488281,只比 int 上限大 4634,n*m/gcd 就输出 -46340 而不是 46341。
四种写法对照:
| 写法 | 代码 | 对不对 | 原因 |
|---|---|---|---|
| 1 | n * m / gcd | ★错★ | int 里先算 n*m → 溢出 |
| 2 | n / gcd * m | ✔ | 先除后乘,中间值不会超过答案 |
| 3 | (long long)n * m / gcd | ✔ | 乘法挪到 long long 里做 |
| 4 | (int)((long long)(n * m) / gcd) | ★错★ | cast 加晚了 —— n * m 已经在 int 里溢出完了 |
实测:n,m = 1~2000 全部 400 万组合法输入 —— 写法2 错 0 处、写法3 错 0 处;而在 999000~1000000 区间里挑出的 1487 组合法输入,写法1 和写法4 全错。
★ 通用规律:把”危险的运算”放到最后一步。
| 场景 | 危险写法 | 安全写法 |
|---|---|---|
| 最小公倍数 | n * m / gcd | n / gcd * m |
| 平均数 | (a + b) / 2(a+b 可能溢出) | a + (b - a) / 2 |
阶乘比值 n!/(n-k)! | 先算两个阶乘 | 先约掉,从 n-k+1 连乘到 n |
| 组合数 | 先算分子再算分母 | 边乘边约 |
“能先约分就先约分” —— 数学上等价的式子,计算顺序不同,安全性完全不同。
⚠️ 还有一种”半途而废”的 long long(写法4):cast 写在最外面,等于什么都没救。要保证乘法本身发生在 long long 里。
“能不能不枚举”—— 范围一大就要先想这句
看到 “小于 10 的 9 次方 / 10 的 12 次方” 这种规模,先问一句:真的要一个个试吗?
第 32 题的两个新套路:
- 求”满足条件的最大值” → 不要从头扫,从上界往下退:
k = n/7*7;然后k -= 7; - 求”某数的倍数” → 用
/和*一步跳过去,不用循环:n/7*7就是”不超过 n 的最大 7 的倍数”
能算出公式/跳着走的,就不要枚举。
★ 第一个算法:辗转相除法(第 38 题)
这是学到的第一个”有名字的算法”。 以前是”把循环和分支拼出来”,从这里开始要问:“这题有没有一个聪明的办法?”
暴力做法为什么不行(实测循环次数):
| 输入 | 枚举法跑几轮 | 辗转相除跑几轮 |
|---|---|---|
4 6 | 4 | 1 |
1000 999 | 999 | 2 |
701408733 433494437 | 433494437(4.3 亿) | 42 |
题目上限 10^9,判题机一秒约跑 1 亿次 → 枚举必然超时。
核心公式:gcd(m, n) = gcd(n, m % n),反复套用到第二个数变成 0,此时第一个数就是答案。
三行骨架(★ 值得背下来):
while (n != 0) { r = m % n; /* ① 先算余数并【存起来】 */ m = n; /* ② 小的那个上位 */ n = r; /* ③ 余数接着当除数 */}printf("%d\n", m); /* n 变 0 时,m 就是答案 */⚠️ 顺序绝对不能反。 写成 m = n; n = m % n; 就错了 —— 实测 gcd(48,18) 输出 18(m 已被覆盖成 18,18 % 18 = 0,第一轮就结束)。
这和斐波那契 int t = a + b; a = b; b = t; 是同一个「滚动变量」套路:先存临时变量,再移动。
两个不用操心的地方:
- 不用特判
m < n:实测4 6第一轮4 % 6 = 4,自动就变成(6, 4)了。 - 不会死循环:每轮
m % n严格小于n,第二个数一路严格变小,必然碰到 0。
为什么”一直除余数”能得到公约数(一句话版):
m % n 的意思就是”把 m 里的 n 一个个减掉,减到减不动为止”。而减去几个 n 不会改变公约数的集合 ——
实测 gcd(48,18) 四步 (48,18)→(18,12)→(12,6)→(6,0),每一步的”公有约数”全都是 1 2 3 6,一个字没变。
更快的口诀记忆:% 就是”更相减损术的快进版”(48→30→12 要减 2 次,48 % 18 = 12 一步到位)。
为什么停在 (m, 0):0 能被任何正整数整除,所以”m 和 0 的公约数”就是”m 的所有约数”,最大的是 m 自己 → gcd(x, 0) = x。
最坏情况是相邻斐波那契数(因为互质)。10^9 以内最大一对 gcd(701408733, 433494437) 要 42 轮。这叫 Lamé 定理:轮数不超过 5 × 位数。
两个前瞻:
- 递归版(等学函数):
int gcd(int a, int b) { return b == 0 ? a : gcd(b, a % b); } - 最小公倍数:
m / gcd * n—— 必须先除后乘,因为m * n在 10^9 量级是 10^18,int装不下 - 扩展欧几里得(以后数论会遇到):能顺便求出
a*x + b*y = gcd(a,b)的x, y,用于求模逆元(RSA 加密)
第 38 题实测记录:400 万对穷举(1~2000)+ 200 万对随机大数(上限 10^9)+ 9 万对与”从大到小枚举”对照 + 42 对相邻斐波那契 + 20 万组倍数性质 → 不一致 0 处;gcc -Wall -Wextra 零警告;gcc -std=c89 -pedantic-errors 通过。
★ 变量声明一律写在 main 开头
第 36 题第 10 行 int yu=0; 写在 scanf 语句之后 —— 这叫”声明与语句混写”,严格 C90 标准不允许:
gcc -std=c89 -pedantic-errors -Wall → error: ISO C90 forbids mixed declarations and code本地 gcc 默认用新标准,只给个警告就过去了;但很多判题机用很老的编译器(VC++ 6.0 或强制 C90 的 gcc),那里这是硬错误,直接判”编译错误”。
改法:把变量全挪到最上面 —— int n = 0, yu = 0;
同理:for (int i = 0; ...) 里的 i 也是 C99 才允许的,老判题机会报错。养成先写 int i; 放最上面的习惯。
★ 命令行坑(不是 C 的问题,但会白耽误时间)
现象:在 PowerShell 里想把”编译 + 运行”一次搞定,写了 cmd / Linux 风格的 && 或 &:
所在位置 行:1 字符: 2+ 2& D:\code\1.exe+ ~表达式或语句中包含意外的标记“&”。 + FullyQualifiedErrorId : UnexpectedToken这不是判题机的报错,是 PowerShell 在抱怨命令本身 —— C 代码一个字都没被执行。
| 写法 | PowerShell 的反应 |
|---|---|
命令1 && 命令2 | 标记“&&”不是此版本中的有效语句分隔符(5.1 不支持 &&,7.0 才有) |
命令1 & 命令2 | 不允许使用与号(&)……是为将来使用而保留的(& 是”调用运算符”,不是分隔符) |
2& 程序.exe | 表达式或语句中包含意外的标记“&” ← 就是这个报错 |
正确写法(二选一):
# 写法 A:分成两行,最省心gcc -Wall -Wextra -o D:\code\1.exe D:\code\1.cD:\code\1.exe
# 写法 B:用分号 ; 连成一行(PowerShell 的语句分隔符是 ;)gcc -Wall -Wextra -o D:\code\1.exe D:\code\1.c ; D:\code\1.exe想直接把输入喂进去: '12354' | D:\code\1.exe
记三句话:PowerShell 分隔语句用 ;;&& 只在 7.0 以后才有;& 是”调用一个命令”的运算符,不是分隔符。
怎么区分报错来源:带 ParserError / CategoryInfo / FullyQualifiedErrorId 字样的是 PowerShell 在说话;判题机的提示只有一行 文件名:行号: error: ...,或者”编译错误 / 答案错误 / 格式错误 / 运行超时”这类中文短语。
六、下一步重点
当前:③ 循环结构
-
while三个零件(起点 / 条件 / 更新) - 计数器、累加器模式
-
for循环(while的紧凑版) - 差一错误(起点 0 配
<,起点 1 配<=) - 打擂台 + 循环
- 多个计数器(统计正/负/零)
- 累乘器(初值 1,不是 0)
- 递推(后一项由前一项算出来:阶乘、平方根数列、a+aa+aaa)
- 递推语句的位置(先造后加 vs 先加后造 —— 要和初值配套)
-
break(找到一个就停 + 顺便防超时) - 标志变量
flag(把”循环里发现的结论”带出循环) - 试除法(判素数:
i从 2 开始,i * i <= n就够) -
long long防溢出 - 哨兵值 / 不定项输入(读到负数为止)
-
%与/配对逐位拆数字(次数不定,一位 0 都不会漏) - 变量声明全部集中在
main开头(兼容老判题机的 C90) - 位权
h(正序拆位:先算最高位位权,再n/h取位、n%h削位、h/=10降权) - 辗转相除法(第一个”有名字的算法”:
while (n) { r = m%n; m = n; n = r; }) -
continue - 循环里嵌套判断
- 循环的不变式(每轮结束时变量应满足什么关系)
核心模式已齐(十种):分段判断、累加器、计数器、打擂台、累乘器、递推器、逆推(倒着算)、滚动变量(二阶递推)、标志变量 flag、试探式 while(先问后加)
拆位两件套(第 36 / 37 题 —— 一正一反,考的是同一对 % 和 /):
| 逆序(第 36 题) | 正序(第 37 题) | |
|---|---|---|
| 骨架 | while (n > 0) { printf("%d ", n % 10); n = n / 10; } | int h=1; while (n/h >= 10) h *= 10;while (h > 0) { printf("%d ", n/h); n = n % h; h /= 10; } |
| 一句话 | 用 /10 把个位扔掉,扔之前先 %10 看一眼 | 用 /h 留下最高位,再用 %h 把它扔掉 |
| 关键 | 除数固定 10 | 必须先花一趟算出 h |
递推的统一骨架:
先把手里有的"变一下"(递推),再把变完的"收起来"(累加)| 题目 | 递推式 |
|---|---|
| 阶乘 | fact = fact * i |
| 阶乘累加 | fact = fact * i; → sum += fact |
| 平方根数列 | x = sqrt(x); → sum += x |
| a+aa+aaa | x = x * 10 + a; → sum += x |
| 斐波那契 | f = a + b; a = b; b = f; |
| 阶乘不等式(第 34 题) | fact *= (m + 1); —— 装的是下一项 (m+1)! |
下一个里程碑:④ 嵌套循环
- 打印三角形/菱形
- 九九乘法表
求最大公约数(辗转相除)✅ 第 38 题已完成求最小公倍数✅ 第 39 题已完成 → 口诀n / gcd * m,先除后乘- 数组(2026-10-21 晚已开课):下标从 0、
int a[100] = {0};清零、大数组放全局、桶cnt[值]++
六点五、★ 被验证过的两件”抓手”(2026-10-21 晚,选拔赛前三天)
抓手一:四句填空法 —— 专治”看得懂但写不出来”
“看懂”不等于”会写”。 每拿到一道输入输出题,只按顺序回答四句话,代码是被这四句话”逼”出来的,不是背出来的:
| 顺序 | 问自己 | 对应代码 |
|---|---|---|
| ① | 读什么?(几个数?什么类型?读到什么为止?) | 声明变量 + scanf |
| ② | 要算出什么?(最终答案存在哪个变量里?) | 定义”答案变量” |
| ③ | 怎么算出来?(分几步?每步用哪几个变量?) | 主循环 / 分支 |
| ④ | 输出什么?(几个?什么格式?要不要 \n?) | printf |
四级台阶(判断自己离”独立写出来”还有多远):
| 台阶 | 给什么 | 能力 |
|---|---|---|
| 1 | 完整代码 | 能看懂 ← 一直停在这里就是无效学习 |
| 2 | 填空架 | 能填 |
| 3 | 只给注释 | 能写 ← 以后从这一级开始 |
| 4 | 只给题目 | 能独立写 ← 考试要的 |
抓手二:变量名用足拼音
英文单字母名(w h best sum)在脑子里是纯噪声,看不出”人数""几点""最多”,于是写代码时”指错对象”。
w + x ← 没有语义差别,只是字母 xiaoshi + shicha ← 差的是一个完整单词,一眼分开三条规则:
- 用完整拼音,不缩写 ——
renshu✔,rs✘ - 一个概念一个名字 —— 分钟用
fenzhong就不要再叫shi i/j保留字母(下标全世界的 C 代码都这么写)
常用角色固定名:计数器 geshu/cishu|累加器 he/zonghe|最大值 zuiduo/zuida|最小值 zuixiao|标志位 biaozhi|临时变量 linshi|数组(桶) tong[]/shuzu[]|长度 changdu
★ 复盘出的核心诊断(比任何语法点都重要)
7-5 四个空全错,但四个错里没有一个是”不会 C”:
| 他的写法 | 应该写 | 错误类型 |
|---|---|---|
local = w + x | dangdi = xiaoshi + shicha | 指错对象(该用循环变量却用了人数) |
sum[h] += 1 | tong[xiaoshi] += renshu | 指错对象(该加”人数”却加了”1 次”) |
sum[h] > sum[h-1] | tong[xiaoshi] > zuiduo | 指错对象(且 h=0 时 sum[-1] 越界) |
local <= 18 | <= 17 | 边界差一(会议开 1 小时) |
结论:短板不是语法,是”精确指称”。 方法:写代码前先列「变量角色表」(每个变量是什么、最容易跟谁混),每写一行先口头念一句”我要用哪个变量”。
★ C 的负数取模(跨时区/环形题必踩)
-3 % 24 = -3 ← C:符号跟着【被除数】走 -3 % 24 = 21 ← Python:不一样!环形下标一律写两层:((x % 24) + 24) % 24
- 第一层
% 24→ 范围(-24, 24),可能为负 + 24→ 范围(0, 48),一定非负- 再
% 24→ 收回0..23
漏掉两层的后果:实测某基地 100 人、时差 x = -20,漏写时 h-20 恒为负 → 这个基地一次都不被算进去 → 答案从 100 变 0。不崩溃、不警告,只是悄悄少算。
七、经典算法地图(遇到一题就点亮一个)
从第 38 题开始,题目会越来越多地考”有名字的算法”。看到题目先问一句:这题有没有一个聪明的办法?
| 算法 | 干什么 | 关键式子 / 骨架 | 状态 |
|---|---|---|---|
| 辗转相除法 | 最大公约数 | while (n) { r = m%n; m = n; n = r; } | ✅ 第 38 题 |
| 埃氏筛 | 快速筛出素数表 | 从 2 开始,把每个素数的倍数全部划掉 | ⬜ 待学 |
| 试除法 | 判断单个数是不是素数 | for (i=2; i*i<=n; i++) | ✅ 第 33 题 |
| 冒泡 / 选择 / 插入排序 | 给一堆数排队 | 两层循环 + 交换 | ⬜ 待学(数组之后) |
| 二分查找 | 在有序数组里找数 | while (lo <= hi) { mid = (lo+hi)/2; ... } | ⬜ 待学 |
| 快速幂 | 算 a^b mod m(b 很大) | 把指数按二进制拆开,平方递推 | ⬜ 待学 |
| 高精度 | 大整数加减乘除 | 用数组存每一位,模拟竖式 | ⬜ 待学 |
| 动态规划 | 有重叠子问题的最优化 | 状态 + 转移方程 | ⬜ 待学 |
| 扩展欧几里得 | 求 a*x + b*y = gcd(a,b) | 在辗转相除上回溯 | ⬜ 待学 |
背模板 + 理解为什么对,两条腿走路。 但背下来不等于写对 —— 第 37 题的教训:算法第一遍就懂了,细节还是错了三轮。每写一次都要问自己”这几行的顺序为什么不能换”。
如果这篇文章对你有帮助,欢迎分享给更多人!
部分信息可能已经过时






