四、循环:计数、累加与遍历

循环是编程真正开始的地方。

前面所有程序都只能处理固定几个数——读两个数、算一个结果。循环让你处理一串数:100 个数求和、1 到 n 里有多少个素数、一个 n 位数的每一位。

这一页 3 章 28 题,核心就三件事:

  1. for 和 while 分别什么时候用
  2. 累加变量的初值该给多少(这是初学者最高频的 bug 来源)
  3. 怎么在一堆数里找最大最小

把这三件事做熟,后面数组、字符串、结构体,全都是在循环上套壳。

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

两题都是热身。第 1 题如果输出少了最后一个数,检查是不是写成了 i < 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");
}

做这类题的固定流程:

  1. 先画一个 n = 4 或 5 的样例
  2. 列表格:行号 / 空格数 / 字符数
  3. 找出「行号 → 数量」的公式
  4. 写代码

跳过第 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。

累加器的三个必查项

  1. 初值给对了吗:累加是 0,累乘是 1,最值是数据的第一个(不是 0)
  2. 类型够大吗:int 最大约 21 亿,1 到 100 万的和就溢出了,要用 long long
  3. 在循环内还是循环外:累加变量声明在循环外,临时变量(比如每轮读入的数)声明在循环内

累加

第 2 题先想清楚:平均值要保留几位小数?用 int 还是 double?

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 开始,因为第一个已经读过了。

最值

做完拿一组全负数的数据自测:-5 -3 -8,最小值应该是 -8。如果你的程序输出 0,就是初值给错了。

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。

累乘与幂

做完查一下:你的 fact 是什么类型?如果写的 int,n = 13 之后就开始出错了。

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」,可能是要求输出格式。

读清楚是「加到某一项为止」还是「输出保留几位」。 这两者写法不同。

级数

两题都要注意:最后输出的精度是题目指定的,用 fixed << setprecision(n)。

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 负责筛选,变量负责累计。

筛选

672 这类「在某个范围内找满足条件的数」,先用纸算几个,确认你理解的规则和题目一致。

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 永远是第一个数,比较就全错了。

这是「需要一个变量记住之前状态」的第一个例子。 后面动态规划、滑动窗口,都是这个思路的升级版。

相邻比较

667 那题涉及百分比变化,注意别用整数除法——第 5 章的坑在这里会再出现一次。

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 是用来「以后查得更深」的,不是用来补课的。

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

results matching ""

    No results matching ""