三、判断:分支结构
这一页 4 章 40 道题。
前面所有题目的执行路径都是同一条:读入、计算、输出。从这一章开始,程序会分叉——同样的输入,走不同的路。
判断本身不难,if 的语法五分钟就能学会。难的是三件事:
- 条件写全了吗——有没有漏掉某种情况
- 顺序对吗——先判断哪个、后判断哪个,结果可能完全不同
- 边界算清楚了吗——「大于」还是「大于等于」,差一个字就错
这三件事,40 道题会反复捶你。
8. 单分支判断
8.1 if:让程序做选择
if (n > 0) {
printf("positive\n");
}
if 后面跟一对括号,括号里是条件,条件成立就执行花括号里的代码,不成立就跳过。
条件是「能判断真假的表达式」,常见的有:
| 写法 | 含义 |
|---|---|
a > b |
大于 |
a >= b |
大于等于 |
a == b |
等于(两个等号) |
a != b |
不等于 |
a % 2 == 0 |
偶数 |
到这里必有一个人踩的坑:
= 和 == 是两个东西
a = 5 是「把 5 放进 a」,a == 5 是「判断 a 是不是 5」。
写成 if (a = 5),编译器不报错(有些会给警告),而且这个条件永远为真——因为赋值表达式的值就是被赋的那个值,5 非零,非零就是真。
更坑的是 if (a = 0):条件永远为假,整段代码永远不执行,你查半天不知道为什么不进分支。
还有一点:if 后面如果只有一条语句,花括号可以省略。但请永远写上花括号。
if (n > 0)
printf("正数\n");
printf("这句话一定会执行\n"); // 缩进骗了你,它不在 if 里
不写花括号时,if 只管紧跟的那一条语句。上面第二行无论条件真假都会执行,但它的缩进让你以为它在里面。
第一个分支
8.2 复合条件:与、或、非
if (a > 0 && a < 10) { } // 与:两个都成立
if (a < 0 || a > 100) { } // 或:有一个成立就行
if (!flag) { } // 非:取反
&& 读作 and,|| 读作 or,! 读作 not。都是两个符号,别写成一个 & 或 |——那是位运算,意思完全不同。
数学里写 0 < a < 10 是合法的,但 C++ 里不能这么写:
if (0 < a < 10) { } // 错!恒为真
if (0 < a && a < 10) { } // 对
为什么错?C++ 从左往右算:先算 0 < a,得到一个真/假的值(0 或 1),再算 1 < 10,结果永远是真。编译器不会拦你,这是最阴险的错之一。
「与」和「或」还有一个短路特性:
if (a != 0 && 100 / a > 5) { } // a 为 0 时,后面不会算,不会崩
if (a == 0 || 100 / a > 5) { } // 同上
&& 左边为假时,右边不再计算;|| 左边为真时,右边不再计算。这不是语法糖,是救命的——上面第一行把 a != 0 放在前面,就避免了除以 0 的崩溃。
复合条件
8.3 多个条件的组合
有时候题目一句话里有好几个条件,要先把中文拆成逻辑:
「恰有一门课不及格」→ 三门课里正好一个分数低于 60
这不能写成 a < 60 || b < 60 || c < 60(那是「至少一门」)。要写成「三个里恰好一个成立」,最直白的写法是枚举三种情况:
int cnt = (a < 60) + (b < 60) + (c < 60);
if (cnt == 1) { ... }
(a < 60) 这个表达式本身就是 0 或 1,三个加起来正好是「不及格的科目数」。这个技巧非常常用,记住它。
「恰有一门」
- 恰有一门课不及格的学生 —— 读题时把「恰有一门」四个字圈出来,然后想:它跟「至少一门」差在哪?
做完了自己加一组数据验证:如果两门都不及格,你的代码输出什么?
第 8 章验收
- 知道
=和==的区别,且再也不会写错 - 知道
0 < a < 10在 C++ 里是错的,必须用&& - 会用
(条件) + (条件)统计满足了几条 - 永远写花括号,哪怕只有一条语句
9. 多分支与复合条件
if 只能处理「要么做、要么不做」。这一章处理「三种以上情况」。
9.1 else 与 else if
if (n > 0) {
printf("正数\n");
} else if (n < 0) {
printf("负数\n");
} else {
printf("零\n");
}
执行逻辑是:从上往下检查,一旦有一个条件成立,执行完就跳出整条链,后面的条件不再检查。
这一点决定了 else if 和「一堆独立的 if」完全不同:
// 写法 A:独立 if,每个都会检查
if (n > 0) printf("正");
if (n > 10) printf("大");
// n = 20 时:输出「正大」
// 写法 B:else if,命中一个就停
if (n > 10) printf("大");
else if (n > 0) printf("正");
// n = 20 时:只输出「大」
什么时候用哪个?
- 情况互斥(一个数不可能既正又负)→ 用
else if - 情况可以叠加(既正又大)→ 用独立
if - 分段函数、成绩等级、星期几 → 一定用
else if
还有一个必须知道的规则:else if 的顺序,从严格到宽松。
// 错:n = 20 会先命中 n > 0,永远不会走到 n > 10
if (n > 0) printf("正");
else if (n > 10) printf("大");
// 对:从窄到宽
if (n > 10) printf("大");
else if (n > 0) printf("正");
多路分支
- 奇偶 ASCII 值判断 —— 先取码值再判奇偶
- 数的性质 —— 题目会给出几种情况,注意判断顺序
- 收集瓶盖赢大奖 —— 条件是「或」,别写成「与」
403 这类题,条件写反是最高频的错。写完把自己的代码读一遍:「题目说的是满足任意一个就行,还是都要满足?」
9.2 逻辑运算符进阶
有些题的条件是多层的,要学会拆:
「输入年份和月份,输出这个月有多少天」
这个条件实际是:小月 30 天、大月 31 天、2 月看闰年。用 if 链写最清楚:
if (m == 2) {
// 看闰年
} else if (m == 4 || m == 6 || m == 9 || m == 11) {
printf("30\n");
} else {
printf("31\n");
}
先处理特例(2 月),再处理一类(小月),最后兜底。 这个结构比列 12 个 if 清楚得多。
逻辑组合
- 洛谷团队系统 —— 先看清题目给的条件关系
- Apples —— 经典的条件判断,注意边界
- 3、5、7 整除判断 —— 要分别判断能不能被每一个整除,还要判断是不是一个都不能
9.3 闰年:条件组合的标准范例
闰年判断是所有教材都会用的例子,因为它把「与」「或」「非」全用上了:
规则:能被 4 整除,但不能被 100 整除;或者能被 400 整除。
bool leap = (y % 4 == 0 && y % 100 != 0) || (y % 400 == 0);
拆开看:
| 年份 | 判断过程 | 结果 |
|---|---|---|
| 2024 | 4 的倍数 ✓,不是 100 的倍数 ✓ | 闰年 |
| 1900 | 4 的倍数 ✓,是 100 的倍数 ✗;不是 400 的倍数 ✗ | 平年 |
| 2000 | 不是 100 的倍数 ✗;是 400 的倍数 ✓ | 闰年 |
1900 和 2000 这两个年份必须验。 它们专门用来抓「只写了 y % 4 == 0」和「忘了 400 那条」的错误。
闰年与月份
9.4 比较与排序
「输出较大的那个」「三个数排序」,本质是两两比较。
两个数取大:
if (a < b) swap(a, b); // 现在 a >= b
swap 是标准库函数,交换两个变量的值。手写就是:
int t = a; a = b; b = t;
三个数排序,最稳的写法是「冒泡」思路:比较 a、b,比较 b、c,再比较 a、b。
if (a > b) swap(a, b);
if (b > c) swap(b, c);
if (a > b) swap(a, b);
三行,覆盖全部 6 种排列。别试图一次写出「智能」的判断,老老实实三次比较最不容易错。
比较与排序
9.5 防御性判断:除数不能为 0
题目如果给了除数,就一定要考虑它是不是 0。
if (b == 0) {
printf("error\n");
} else {
printf("%d\n", a / b);
}
不判断的后果是程序直接崩溃(运行时错误),这比答案错误更亏——因为你会以为是算法错了,查半天发现是除零。
浮点数不能用 == 比较
double x = 0.1 + 0.2;
if (x == 0.3) { } // 永远不会成立!
0.1 + 0.2 在浮点数里是 0.30000000000000004。要比较两个浮点数是否相等,得写成:
if (fabs(x - 0.3) < 1e-9) { }
判断大小(>、<)没这个问题,只有「相等」要注意。
除法与边界
- A 除以 B —— 先判断 B 是不是 0
这题通常会要求输出特定内容(比如 error 或 divided by zero),照抄题目的字符串,大小写都别改。
第 9 章验收
- 能说清
else if和独立if的执行差别 - 知道
else if的顺序要从窄到宽 - 能默写闰年判断,且知道用 1900 和 2000 验
- 写除法前会先想「除数可能是 0 吗」
10. 分段函数与阈值
这一章全是「不同的数,走不同的路」——数学上叫分段函数,生活里叫阶梯计费。
10.1 分段函数:把数轴切成区间
分段函数的特点是:每一段有每一段的公式。写法就是 else if 链,关键是区间不能重叠也不能漏。
// 符号函数:正数返回 1,负数返回 -1,零返回 0
if (x > 0) printf("1\n");
else if (x < 0) printf("-1\n");
else printf("0\n");
这个写法之所以对,是因为三段正好覆盖整个数轴:>0、<0、剩下的必然是 =0。
写分段函数时,拿张纸画一条数轴,把每段标上去。画完再写代码。 这一步花 30 秒,能省你半小时调试。
简单分段
- 符号函数计算 —— 就是上面那个例子
- 分段函数 —— 区间可能有三四段,先画数轴
- 百分制转五分制成绩转换 —— 注意边界的开闭(90 分是 A 还是 B?)
10.2 阶梯计费:逐段累加
这是本章的难点。阶梯水费、阶梯电费、个人所得税,都是同一个模型:
前 100 度每度 0.5 元,100~200 度每度 0.6 元,200 度以上每度 0.8 元。
错误写法(很多人第一反应):
if (x <= 100) ans = x * 0.5;
else if (x <= 200) ans = x * 0.6; // 错!前 100 度也按 0.6 算了
正确做法是逐段算,然后加起来:
double ans = 0;
if (x <= 100) {
ans = x * 0.5;
} else if (x <= 200) {
ans = 100 * 0.5 + (x - 100) * 0.6; // 前 100 度 + 超出部分
} else {
ans = 100 * 0.5 + 100 * 0.6 + (x - 200) * 0.8;
}
注意第二行:100 * 0.5 是前 100 度按第一档算的钱,(x - 100) * 0.6 是超出部分按第二档算的钱。
另一种更通用的写法(段数多的时候好用):
double ans = 0;
int last = 0; // 上一档的上限
double price[] = {0.5, 0.6, 0.8};
int upper[] = {100, 200, 100000};
for (int i = 0; i < 3; i++) {
if (x > last) {
int use = min(x, upper[i]) - last; // 这一段用了多少
ans += use * price[i];
last = upper[i];
}
}
这种写法把「档位」变成数组,加一档只需改数组,不用改逻辑。第 22 章学数组后会常用。
阶梯题的三个死法
- 整段乘单价(上面那个错误写法)——最常见
- 边界重复:
x > 100和x >= 100混用,100 度算两次 - 整型转小数丢失:
ans声明成int,算出来全是整数
交之前拿 x = 100、x = 200 各跑一遍,这两个点最容易错。
阶梯计费
10.3 阈值判定
比大小、超没超线,是最简单的一类判断,但边界值最容易错:
- 「超过 60」是
> 60还是>= 60? - 「不低于 60」是
>= 60 - 「超过标准的 20% 以上」要算清楚比的是谁
10.4 几何位置判断
点在不在图形里、线能不能组成三角形,考的是几何条件能不能翻译成代码。
三角形:任意两边之和大于第三边。
if (a + b > c && a + c > b && b + c > a) { /* 能构成 */ }
三条都要写。 只写 a + b > c 是不够的——如果 c 特别大,其他两条也可能不满足。
点是否在矩形内,就是四个不等式同时成立:
if (x >= x1 && x <= x2 && y >= y1 && y <= y2) { }
10.5 闰年再练
再判一次闰年
- 判断闰年 —— 和第 9 章的 26 是同一类题,隔了一章再写一遍,看你还记不记得
这次不看书,直接写。写不出来说明 9.3 没记住,回去补。
第 10 章验收
- 写分段函数前会先画数轴
- 知道阶梯计费要「逐段算再相加」,不能整段乘
- 边界值(100、60、0)会单独验
- 三角形判断写全三条不等式
11. 判断的综合应用
前面三章把工具都给齐了,这一章考的是能不能把题目描述完整地翻译成条件。
11.1 把情况列全
复杂判断题的通用流程:
- 把题目里所有「如果……就……」的句子抄出来,编号
- 数一数一共几种情况
- 检查这些情况有没有遗漏、有没有重叠
- 才动手写代码
举个典型:简易计算器。
int a, b; char op;
scanf("%d %c %d", &a, &op, &b);
if (op == '+') printf("%d\n", a + b);
else if (op == '-') printf("%d\n", a - b);
else if (op == '*') printf("%d\n", a * b);
else if (op == '/') {
if (b == 0) printf("error\n");
else printf("%d\n", a / b);
}
四种运算符是四种情况,一个都不能漏。而且除法里面还嵌了一个判断——嵌套的 if 在这里是必要的。
顺便看那行 scanf("%d %c %d", &a, &op, &b):%d 和 %c 之间那个空格不是摆设。输入 3 + 5 时,%d 读完 3 就停下了,下一个字符是空格;格式串里的空格会先跳过所有空白,%c 才读到真正的 +。写成 "%d%c%d",%c 就读到那个空格了——这是第 3 章 %c 那个坑的另一个版本。
列全所有情况
11.2 二选一:算出两个再比
有些题是「两种方案哪个更优」,最直接的写法是两个都算出来,再比较:
int costA = ...;
int costB = ...;
if (costA < costB) printf("A\n");
else printf("B\n");
不要试图用一堆条件直接推出答案——那种写法难写又难改。先算再比,永远是最稳的。
11.3 区间与周期
落在某个区间里、每几天循环一次,都是取模 + 比较的组合:
// 每 5 天一个周期,问第 n 天是周期里的第几天
int d = (n - 1) % 5 + 1; // 1~5
(n-1) % k + 1 这个式子把任意正整数映射到 1~k,是循环题的万能公式,记下来。
区间与周期
11.4 综合
剩下的综合题
第 11 章验收
40 道判断做完,你应该达到:
- 拿到一道题,先把所有情况列出来再写代码,而不是边写边想
- 知道「至少一个」和「恰好一个」的写法区别
- 会画表格处理多维度组合的条件
- 边界值(0、100、60、等于号)会主动验
判断板块是后面所有内容的地基。 循环里的「满足条件就计数」、数组里的「找最大值」、函数里的「判断素数」,内核都是这一页的东西。这里不熟,后面每章都会卡。
延伸阅读
服务器上有一整套 OI-Wiki(621 页),是给竞赛选手写的参考手册。下面几页跟这一页相关,但不是每一页都该现在看——标了「现在跳过」的,等以后再说。
- 可以先看:分支 ——
if和if...else的结构图比文字清楚
看不懂是正常的,不是你的问题
OI-Wiki 默认读者已经会写程序、目标是打 NOI。你现在需要的那部分,讲义里已经写了; wiki 是用来「以后查得更深」的,不是用来补课的。