三、判断:分支结构

这一页 4 章 40 道题。

前面所有题目的执行路径都是同一条:读入、计算、输出。从这一章开始,程序会分叉——同样的输入,走不同的路。

判断本身不难,if 的语法五分钟就能学会。难的是三件事:

  1. 条件写全了吗——有没有漏掉某种情况
  2. 顺序对吗——先判断哪个、后判断哪个,结果可能完全不同
  3. 边界算清楚了吗——「大于」还是「大于等于」,差一个字就错

这三件事,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 只管紧跟的那一条语句。上面第二行无论条件真假都会执行,但它的缩进让你以为它在里面。

第一个分支

399 有个细节:负数取余。-3 % 2 在 C++ 里是 -1 不是 1,所以判断偶数要写 n % 2 == 0(这个对负数也对),别写 n % 2 == 1。

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 的崩溃。

复合条件

第 2 题如果想让代码更短:n = (n < 0) ? -n : n;,这叫三目运算符。看得懂就行,不强求用。

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("正");

多路分支

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 清楚得多。

逻辑组合

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 那条」的错误。

闰年与月份

60 这题建议自己验一遍:输入 1900 2 应该输出 28,输入 2000 2 应该输出 29。两个都对,才算过。

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 种排列。别试图一次写出「智能」的判断,老老实实三次比较最不容易错。

比较与排序

77 这题的顺序很重要:先判断能不能构成三角形,再判断等边/等腰/直角。顺序反了,非三角形会被判成某种类型。

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) { }

判断大小(>、<)没这个问题,只有「相等」要注意。

除法与边界

这题通常会要求输出特定内容(比如 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 秒,能省你半小时调试。

简单分段

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 章学数组后会常用。

阶梯题的三个死法

  1. 整段乘单价(上面那个错误写法)——最常见
  2. 边界重复:x > 100 和 x >= 100 混用,100 度算两次
  3. 整型转小数丢失: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) { }

几何判断

413 这类题,题目的「在内部」「在边上」「在外部」三种情况要分别处理,边界归属看清题目怎么说。

10.5 闰年再练

再判一次闰年

  • 判断闰年 —— 和第 9 章的 26 是同一类题,隔了一章再写一遍,看你还记不记得

这次不看书,直接写。写不出来说明 9.3 没记住,回去补。

第 10 章验收

  • 写分段函数前会先画数轴
  • 知道阶梯计费要「逐段算再相加」,不能整段乘
  • 边界值(100、60、0)会单独验
  • 三角形判断写全三条不等式

11. 判断的综合应用

前面三章把工具都给齐了,这一章考的是能不能把题目描述完整地翻译成条件。

11.1 把情况列全

复杂判断题的通用流程:

  1. 把题目里所有「如果……就……」的句子抄出来,编号
  2. 数一数一共几种情况
  3. 检查这些情况有没有遗漏、有没有重叠
  4. 才动手写代码

举个典型:简易计算器。

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 那个坑的另一个版本。

列全所有情况

  • 简单计算器 —— 先在心里列一遍有几种运算符
  • 计算邮资 —— 通常有「重量段 × 是否加急」两个维度,要组合起来

408 这类两个维度组合的题,画个表格最清楚:行是重量档,列是加急与否。表格填满,代码就出来了。

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,是循环题的万能公式,记下来。

区间与周期

417 的日期比较,有个偷懒办法:把日期转成一个整数 y*10000 + m*100 + d,然后直接比大小。日期题里这招很好用。

11.4 综合

剩下的综合题

遇到读不懂的题,先把样例抄下来,手算一遍。 手算过程走通了,代码就是把手算过程翻译一遍。

第 11 章验收

40 道判断做完,你应该达到:

  • 拿到一道题,先把所有情况列出来再写代码,而不是边写边想
  • 知道「至少一个」和「恰好一个」的写法区别
  • 会画表格处理多维度组合的条件
  • 边界值(0、100、60、等于号)会主动验

判断板块是后面所有内容的地基。 循环里的「满足条件就计数」、数组里的「找最大值」、函数里的「判断素数」,内核都是这一页的东西。这里不熟,后面每章都会卡。

延伸阅读

服务器上有一整套 OI-Wiki(621 页),是给竞赛选手写的参考手册。下面几页跟这一页相关,但不是每一页都该现在看——标了「现在跳过」的,等以后再说。

  • 可以先看:分支 —— if 和 if...else 的结构图比文字清楚

看不懂是正常的,不是你的问题

OI-Wiki 默认读者已经会写程序、目标是打 NOI。你现在需要的那部分,讲义里已经写了; wiki 是用来「以后查得更深」的,不是用来补课的。

© HiJudge · 内容采用 CC BY-NC-SA 4.0 许可协议            此页面修订于: 2026-11-01 03:44:09

results matching ""

    No results matching ""