四、循环:计数、累加与遍历
循环是编程真正开始的地方。
前面所有程序都只能处理固定几个数——读两个数、算一个结果。循环让你处理一串数:100 个数求和、1 到 n 里有多少个素数、一个 n 位数的每一位。
这一页 3 章 28 题,核心就三件事:
for和while分别什么时候用- 累加变量的初值该给多少(这是初学者最高频的 bug 来源)
- 怎么在一堆数里找最大最小
把这三件事做熟,后面数组、字符串、结构体,全都是在循环上套壳。
12. 计数循环与图形输出
12.1 for:重复做同一件事
for (int i = 1; i <= n; i++) {
printf("%d\n", i);
}
for 括号里三段,用分号隔开:
| 位置 | 作用 | 例子 |
|---|---|---|
| 第一段 | 初始化,只执行一次 | int i = 1 |
| 第二段 | 条件,每轮开始前检查 | i <= n |
| 第三段 | 每轮结束后执行 | i++(等于 i = i + 1) |
执行顺序是:初始化 → 检查条件 → 循环体 → 第三段 → 检查条件 → 循环体 → ……
循环从 1 开始还是从 0 开始? 数数用 1 到 n(i = 1; i <= n),数组下标用 0 到 n-1(i = 0; i < n)。这两种写法都常见,看场景。
一个必须警惕的错:循环条件写错,会多跑或少跑一轮。
for (int i = 1; i < n; i++) // 少一轮,n 本身没取到
for (int i = 1; i <= n; i++) // 对,1 到 n
差一个等号,结果就少一个数。遇到「答案差一点」的情况,第一个怀疑对象就是循环边界。
从 1 数到 n
12.2 循环里控制输出:画图形
图形输出题考的不是循环本身,而是「第 i 行该输出几个字符」这个规律能不能找出来。
以等腰三角形为例,n = 4 时:
*
***
*****
*******
每行的规律:
| 行号 i | 空格数 | 星号数 |
|---|---|---|
| 1 | 3 | 1 |
| 2 | 2 | 3 |
| 3 | 1 | 5 |
| 4 | 0 | 7 |
规律是:空格 = n - i,星号 = 2i - 1。
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n - i; j++) printf(" ");
for (int j = 1; j <= 2 * i - 1; j++) printf("*");
printf("\n");
}
做这类题的固定流程:
- 先画一个 n = 4 或 5 的样例
- 列表格:行号 / 空格数 / 字符数
- 找出「行号 → 数量」的公式
- 写代码
跳过第 2 步直接写代码的人,90% 会卡住。因为图形题的公式不是想出来的,是数出来的。
12.3 双重循环
一个循环里套另一个循环,用于「每一行还要做很多事」。
最典型的例子是九九乘法表:
for (int i = 1; i <= 9; i++) {
for (int j = 1; j <= i; j++) {
printf("%dx%d=%d\t", j, i, j * i);
}
printf("\n");
}
注意内层是 j <= i,不是 j <= 9——第 i 行只有 i 个式子,这就是三角形的形状。
双重循环的执行次数 = 外层次数 × 内层次数。 外层 9 次、内层最多 9 次,总共 45 次。这个概念在第 15 章判断素数时会用到(为什么试除到根号就够了)。
双重循环
- 九九乘法表 —— 注意题目要求的格式(分隔符、对齐方式)
12.4 while:次数不确定的时候
for 适合「我知道要循环几次」,while 适合「我不知道几次,但知道什么时候停」。
int n;
scanf("%d", &n);
while (n > 1) {
if (n % 2 == 0) n = n / 2;
else n = n * 3 + 1;
cnt++;
}
这段是角谷猜想(第 20 章会做)。你不知道要走多少步,只知道「只要还大于 1 就继续」。
while 的两种写法等价:
while (n > 1) { ... }
for (; n > 1; ) { ... } // 三段的中间那一段就叫 while
选哪种看可读性:循环次数已知或可数 → for;循环次数未知、靠条件退出 → while。
while 最怕死循环
while (n > 1) {
n = n * 2; // n 永远大于 1,循环停不下来
}
程序不会崩,它会一直跑到超时(OJ 报 TLE),或者在你本机上刷屏。
写 while 时,一定要能回答一个问题:循环体里有没有让条件最终变假的操作? 如果答不上来,就会死循环。
次数不确定的循环
- 一尺之棰的长度变化天数 —— 每天砍一半,问几天后不足某个长度
- 苹果记录问题
- 分类平均
三道题都要先想清楚:循环条件是什么?循环体里哪个操作会让条件最终不成立?
第 12 章验收
- 能默写
for的三段结构,并解释执行顺序 - 知道
i < n和i <= n的区别,以及什么时候用哪个 - 图形输出题会先列表格找规律,而不是硬想公式
- 写
while时会检查「条件会不会最终变假」
13. 累加、累乘与最值
这一章是循环里最实用的三件套,也是 bug 最密集的地方。
13.1 累加器:从 0 开始
int sum = 0; // 关键:初值 0
for (int i = 1; i <= n; i++) {
sum += i; // 等于 sum = sum + i
}
sum += i 是 sum = sum + i 的简写。-=、*=、/= 同理。
累加的初值必须是 0。这不是习惯,是数学要求:0 加上任何数都不变,所以从 0 开始才能正确累加。
int sum; // 错!未初始化,值是随机的
for (...) sum += i; // 结果是一个随机数
未初始化的局部变量,值是内存里残留的垃圾。它可能每次都不一样——本地跑对了、交上去错了,是最难查的那种 bug。
累加器的三个必查项
- 初值给对了吗:累加是 0,累乘是 1,最值是数据的第一个(不是 0)
- 类型够大吗:
int最大约 21 亿,1 到 100 万的和就溢出了,要用long long - 在循环内还是循环外:累加变量声明在循环外,临时变量(比如每轮读入的数)声明在循环内
13.2 最值的初值:这是本章最大的坑
找最大值,标准写法:
int mx = -1000000000; // 或者 -1e9,或者 INT_MIN
for (int i = 1; i <= n; i++) {
int x; scanf("%d", &x);
if (x > mx) mx = x;
}
初值为什么不能给 0? 因为如果所有数都是负数,mx 会一直是 0,而 0 根本不在数据里。
int a[] = {-5, -3, -8};
int mx = 0; // 错!结果输出 0,正确答案是 -3
三种正确写法,选一种:
| 写法 | 说明 |
|---|---|
int mx = INT_MIN; |
需要 #include <climits>,但 bits/stdc++.h 已经包含了 |
int mx = -1e9; |
简单粗暴,只要数据不小于 -10 亿就安全 |
| 读第一个数作为初值 | 最保险,但要单独处理「第一个数」 |
第三种写法最严谨:
int n; scanf("%d", &n);
int mx; scanf("%d", &mx); // 先读第一个
for (int i = 2; i <= n; i++) {
int x; scanf("%d", &x);
if (x > mx) mx = x;
}
注意循环从 i = 2 开始,因为第一个已经读过了。
最值
13.3 累乘与幂
累乘和累加一模一样,只有初值不同:累乘从 1 开始。
long long fact = 1; // 关键:初值 1,不是 0
for (int i = 1; i <= n; i++) {
fact *= i;
}
初值给 0 的话,乘什么都是 0。
阶乘增长极快:13! 就超过 int 上限了。所以阶乘题一律用 long long,而且题目通常会限制 n 的范围。
求 a 的 n 次方,原理相同:
long long p = 1;
for (int i = 1; i <= n; i++) p *= a;
别用 pow(a, n):它返回 double,结果大于 2^53 就开始丢精度,而且转回整数时可能差 1。
13.4 级数求和与精度
e = 1 + 1/1! + 1/2! + 1/3! + ...
这类题的写法是边算边累加,直到某一项足够小:
double e = 1.0, term = 1.0;
int i = 1;
while (term > 1e-10) {
term /= i; // 上一项除以 i 得到这一项
e += term;
i++;
}
注意 term /= i 这个技巧:1/i! = 1/(i-1)! ÷ i,所以不用每次重算阶乘,用上一项除以 i 就行。这比每次都算阶乘快得多,也避免阶乘溢出。
级数题的循环条件
「精度到 1e-6」不等于「循环到 term > 1e-6」。
题目说「最后一项小于 1e-6 时停止」,那条件就是 term >= 1e-6;说「结果精确到 1e-6」,可能是要求输出格式。
读清楚是「加到某一项为止」还是「输出保留几位」。 这两者写法不同。
13.5 组合:最大跨度
跨度
- 最大跨度值 —— 一般指「后面的数减前面的数」的最大值,不是最大值减最小值
这题容易理解错。如果题目说的是「最大跨度 = 最大值 - 最小值」,那和第 164 题一样;如果是「某段区间内的最大差」,就得边遍历边维护。读题,别猜。
第 13 章验收
- 累加初值 0、累乘初值 1、最值初值不能是 0 —— 三条都记住
- 知道
int什么时候不够用,什么时候该上long long - 级数求和会用「上一项除以 i」而不是每次重算
- 找最值时会用全负数数据自测
14. 序列遍历与条件筛选
这一章的模式是:读入一串数,边读边处理,不存下来。
14.1 边读边算
大多数题不需要把数据全部存起来。读到就处理,处理完就扔:
int n; scanf("%d", &n);
int sum = 0;
for (int i = 0; i < n; i++) {
int x; scanf("%d", &x);
sum += x;
}
printf("%d\n", sum);
这样做的好处是用多少内存都行——100 万个数也只占一个变量的空间。
什么时候必须存下来? 当后面的处理需要回头访问前面的数时。比如「找出最大值的位置」「判断是否递增」「排序」。这些要到第 22 章数组才讲。
14.2 筛选与计数
「满足条件的数有几个」「把满足条件的都输出」,模式是在循环里加一个 if:
int cnt = 0;
for (int i = 1; i <= n; i++) {
if (i % 3 == 0 && i % 5 == 0) {
cnt++;
}
}
这个模式后面会用到无数次:素数计数、闰年计数、成绩分段统计……循环负责遍历,if 负责筛选,变量负责累计。
筛选
14.3 相邻比较:需要「记住上一个」
「是否连续上升」「连续相同的有多长」——这类题的关键是保存上一个数:
int prev, cur;
scanf("%d", &prev);
for (int i = 2; i <= n; i++) {
scanf("%d", &cur);
if (cur > prev) { /* 上升 */ }
prev = cur; // 关键:更新 prev
}
prev = cur 这一行千万别忘。忘了的话,prev 永远是第一个数,比较就全错了。
这是「需要一个变量记住之前状态」的第一个例子。 后面动态规划、滑动窗口,都是这个思路的升级版。
相邻比较
14.4 标记法:用一个变量记住「有没有发生过」
有时候不需要计数,只需要知道「有没有」:
bool found = false;
for (int i = 1; i <= n; i++) {
if (满足条件) {
found = true;
break; // 找到了就可以停
}
}
if (found) printf("yes\n");
else printf("no\n");
bool 类型只有 true 和 false 两个值。用 int 存 0/1 也行,但 bool 更能表达意图。
break 是立刻跳出当前这一层循环。找到了就停,不用继续跑完剩下的——这在数据量大时能省很多时间。
标记与提前退出
- 病毒检测 —— 典型的「找到一个就够」
做完想想:如果不用 break,程序还能对吗?能对,但会慢。什么时候该加 break,是第 15 章判断素数时要认真讨论的问题。
第 14 章验收
- 能不存数组就算出总和、最大值、满足条件的个数
- 知道「需要记住上一个」时该怎么做
- 会用
bool标记 +break提前退出 - 循环条件和题目描述的对应关系,能一眼看出来
到这里,循环的基本功就齐了。 接下来两页会用这些工具解决真正的问题:素数、数位、因数、递推、模拟。工具不熟的话,回去把这三章的题再刷一遍——在更复杂的题里补基础,代价要大得多。
延伸阅读
服务器上有一整套 OI-Wiki(621 页),是给竞赛选手写的参考手册。下面几页跟这一页相关,但不是每一页都该现在看——标了「现在跳过」的,等以后再说。
- 可以先看:循环 ——
for的三个部分分别什么时候执行——这个必须搞清楚,不然嵌套循环必错
看不懂是正常的,不是你的问题
OI-Wiki 默认读者已经会写程序、目标是打 NOI。你现在需要的那部分,讲义里已经写了; wiki 是用来「以后查得更深」的,不是用来补课的。