二、计算:变量、运算与输入输出

这一页 5 章 49 道题,是整个讲义里题量最大的一页。

它只解决一件事:把题目给的数据读进来,按题目说的关系算出去。

听起来简单,但这一页会暴露初学者最多的两类问题:一是把题目里的数字直接手算成答案写死在代码里,二是算出来了但格式不对(小数位、整除、单位)。这两类问题在后面每一章都会重复出现,所以在这里必须彻底解决。

学完这一页,你应该能拿到任何一道「给几个数、求一个结果」的题,十分钟内写出能过样例的代码。

3. 变量与整数输入

第 1、2 章的数据都是你写死的。这一章开始,数据由题目在运行时给你。

3.1 变量:给数据起个名字

int a;          // 声明一个叫 a 的整数变量
a = 5;          // 把 5 放进去
int b = 3;      // 声明的同时放进去

int 是 integer 的缩写,表示整数。变量名是给内存起的名字,后面用名字就能取到里面的数。

命名的硬规则只有三条:只能用字母、数字、下划线;不能数字开头;不能和 int、return、for 这些关键字重名。另外 C++ 区分大小写,a 和 A 是两个不同的变量。

软规则是:题目只有两三个数时用 a b c 没问题;变量一多,sum、age、total 这种名字能让你三天后还看得懂自己写了什么。

3.2 scanf:把数据读进来

int a, b;
scanf("%d %d", &a, &b);

scanf 和 printf 是镜像的,对比着记最快:

printf scanf
格式串 "%d\n" "%d"
后面的参数 变量本身:x 变量的地址:&x
换行 要自己写 \n 千万别写 \n

第一个坑:& 不能丢

scanf("%d", &a);    // 对
scanf("%d", a);     // 错,程序会崩溃

& 是「取地址」。printf 是把值送出去,所以告诉它值就行;scanf 是把值接收进来,必须告诉它「放到内存的哪个位置」,所以给地址。

漏掉 & 的后果不是编译错误,是运行时崩溃或者读到奇怪的值。这是 scanf 最高频的错误,没有之一。

第二个坑:格式串里不要写 \n

scanf("%d\n", &a);   // 错
scanf("%d", &a);     // 对

scanf 的格式串描述的是「要读什么」,不是「要输出什么」。写 \n 会让它去匹配一个换行符,匹配不上就一直等——表现是程序卡在那里,光标闪着,就是不往下走。

很多人第一次遇到这个,会以为是自己电脑坏了。

格式符对照表

变量类型 scanf 用 printf 用 例子
int %d %d scanf("%d", &a);
long long %lld %lld scanf("%lld", &x);
float %f %f scanf("%f", &f);
double %lf %f scanf("%lf", &d);
char %c %c scanf("%c", &c);
字符数组 %s %s scanf("%s", s);

这张表里有三处会咬人,提前说:

double 的读写用的不是同一个格式符

double d;
scanf("%lf", &d);      // 读:%lf
printf("%f\n", d);     // 写:%f

scanf 读 double 必须 %lf(l 是 long,表示这个是 8 字节的浮点数)。用了 %f 会只写进去 4 个字节,剩下 4 个字节是垃圾值,读出来是个天文数字。

而 printf 输出 double 用 %f 就行——printf 看到浮点参数会自动提升成 double,所以 %f 通吃 float 和 double。

一句话:读 double 用 %lf,写 double 用 %f。 不对称,记住。

%c 会读到空格和换行

这个坑要到第 31 章才算真正解决,但你现在就应该知道:

int n; char c;
scanf("%d", &n);
scanf("%c", &c);    // 读到的很可能是上一行末尾的换行符!

输入 5 然后回车时,scanf("%d") 只吃掉了 5,回车还留在缓冲区里。下一个 %c 就把这个回车读走了。

标准解法是在 %c 前面加一个空格:scanf(" %c", &c);。那个空格的意思是「先跳过所有空白字符」。

以后凡是 %c,前面都加空格,养成习惯就不会错。

%s 读字符串时不加 &,因为数组名本身就是地址。这个到第 31 章再说。

多个输入

int a, b, c;
scanf("%d %d %d", &a, &b, &c);

格式串里的空格不表示「必须有一个空格」,它表示「跳过任意数量的空白(空格、换行、制表符)」。所以下面两种输入都能正确读进三个数:

3 5 8
3
5
8

实际写的时候,%d%d%d 和 %d %d %d 效果完全一样。加空格只是为了你自己看得清楚。

为什么这本讲义用 scanf 而不是 cin

C++ 里还有一种写法 cin >> a >> b;,不用写 &、不用管格式符,看起来更省事。但还是建议你用 scanf,三个理由:

  1. 快。 cin 默认和 C 的输入输出同步,读大量数据时慢 3–5 倍。第 12 章开始你会读几万个数,这个差距就是「过」和「超时」。
  2. 能精确控制格式。 scanf("%d:%d", &h, &m) 可以直接读 12:30 这种数据,用 cin 得自己处理冒号。
  3. 和 printf 配套。 输入输出的写法统一,不用在两套语法之间来回切换。

如果哪天你非要用 cin,记得在 main 开头写 ios::sync_with_stdio(false); cin.tie(0); 关掉同步。但既然都要写这行了,不如直接用 scanf 干净。

第一次读输入

这道题就是读两个数、输出和。故意把 scanf 和 printf 各写一行,别图省事压成一行——后面题目复杂了你会感谢自己。

如果本地跑出来是 0、随机数,或者干脆崩溃,按这个顺序查:

  1. & 写了吗?
  2. 格式串是不是写成了 "%d\n"?
  3. printf 的坑数和参数个数对得上吗?

3.3 别手算,让表达式算

初学者最常犯的错:看到「3 个苹果每个 5 元」,就在代码里写 printf("15\n");。样例能过,一交就错——题目换组数据你就全错。

正确做法是把题目的关系翻译成表达式:

int n, price;
scanf("%d %d", &n, &price);
printf("%d\n", n * price);

题目说「n 个、每个 price 元」,代码就写 n * price。输入怎么变都对。

翻译时最常出错的是括号。数学里写 (a+b)×c 有括号,代码里必须也写括号:

printf("%d\n", (a + b) * c);   // 对
printf("%d\n", a + b * c);     // 错:先算了 b*c

把关系翻译成表达式

四道题,难度是递增的。做的时候心里默念一遍「题目说的是什么关系」,再落笔。

第 4 题可能会让你犹豫:(a+b)/c 如果除不尽怎么办?先按整数除法做,看看样例对不对——下一节正好讲这个。

3.4 整除和取余:题目里的两种「除」

生活里的除法有两种问法,C++ 里对应两个符号:

题目里的问法 用 例子
能装几袋 / 每人几个 / 能做几次 / 17 / 5 = 3
还剩几个 / 余多少 % 17 % 5 = 2
int total = 17, per = 5;
printf("%d\n", total / per);   // 3  能装满几袋
printf("%d\n", total % per);   // 2  还剩几个

整数除法直接砍掉小数,不四舍五入。 7 / 2 是 3,1 / 2 是 0。这不是 bug,是规则。

还有一条极其隐蔽的坑,几乎每个人都会踩一次:

int f = 100;
printf("%d\n", 5 * (f - 32) / 9);   // 37  对
printf("%d\n", 5 / 9 * (f - 32));   // 0   错!

第二行错在:先算 5 / 9,两个整数相除得 0,再乘什么都还是 0。

结论:连乘连除时,先乘后除。 把除法留到最后一步,中间结果才不会被截断。

整除、取余,还有那个 0

  • 带余除法 —— 这题同时要商和余数,两个符号都用上
  • 年龄问题 —— 想清楚哪一步该用 /,哪一步该用 %
  • 华氏温度转摄氏 —— 公式里有个 5/9。先乘后除,否则结果恒为 0

第 3 题如果一直输出 0,回去看上面那个例子。

第 3 章验收

  • 能用 scanf("%d %d", &a, &b); 读入两个整数
  • 能把「先加后乘」正确地翻译成带括号的表达式
  • 分得清 / 和 % 各回答什么问题
  • 知道 5 / 9 等于 0,所以要先乘后除

4. 整数运算与数字处理

这一章所有题都围绕一件事:把一个整数的每一位拆出来,再拼回去。

为什么重要?因为后面判断回文、数位统计、进制转换、日期处理,全是从这里长出来的。

4.1 拆出每一位:/10 和 %10

一个十进制数 152,拆法是这样的:

int n = 152;
printf("%d\n", n % 10);        // 2   个位
printf("%d\n", n / 10);        // 15  去掉个位
printf("%d\n", n / 10 % 10);   // 5   十位
printf("%d\n", n / 100);       // 1   百位

记这两条,比记公式有用:

  • % 10 取出最后一位
  • / 10 丢掉最后一位

反复做这两步,就能从右往左走完整数:

int n = 152;
while (n > 0) {
    printf("%d ", n % 10);   // 2 5 1
    n = n / 10;
}

这个 while 循环第 16 章会正式讲,现在先看懂它的意思:只要还有数位,就取出最后一位、再砍掉它。

拆一个三位数

这题要求你把一个三位数的百位、十位、个位分别算出来。

提示:百位是 /100,个位是 %10,十位两种写法都对(/10%10 或 %100/10)。自己推一遍,别背。

4.2 拼回去:逆序与重组

拆出来之后,重新拼成一个新数,靠的是乘 10 加:

// 把 1、5、2 拼成 152
int x = 1;
x = x * 10 + 5;   // 15
x = x * 10 + 2;   // 152

x = x * 10 + d 这个动作,就是把 x 整体左移一位,再在末尾填上 d。

逆序的道理一样,只是取位顺序反了:从右往左取,就往右往左填。

把数倒过来

三道题做完,你应该能回答:为什么 x = x * 10 + d 能实现「拼接」?

4.3 加密、日期、格式:把数位当零件用

这一类题不再求「和是多少」,而是把每一位当成零件,按题目给的规则重新组装。

比如四位数加密、日期 20260315 拆成 2026-03-15,本质都是同一件事:拆位 → 按规则处理 → 拼回。

int d = 20260315;
int y = d / 10000;          // 2026
int m = d / 100 % 100;      // 3
int day = d % 100;          // 15

注意 m 那一行:d / 100 得到 202603,再 % 100 拿到最后两位 03。这是「先砍掉尾巴、再取末尾」的组合技,比硬算简单。

数位当零件

输出补 0 用 %02d(第 5 章细讲):

printf("%d-%02d-%02d\n", y, m, day);

%02d 的意思是「宽度 2 位,不够的左边补 0」。日期、时间题全用这个写法。

4.4 整除的坑:不够一份也要算一份

上一章讲过 / 是「能装满几份」。但题目经常问的是「至少需要几份」——比如 17 个苹果每袋装 5 个,需要 4 袋而不是 3 袋。

这两种问法差一个「向上取整」,写法完全不同:

int n = 17, per = 5;
printf("%d\n", n / per);               // 3  能装满几袋(向下取整)
printf("%d\n", (n + per - 1) / per);   // 4  至少需要几袋(向上取整)

(n + per - 1) / per 是整数向上取整的标准写法。它的道理是:先在总数上补「差一点点就够一份」的量,再整除,就把余数也顶成一份了。

别用 ceil()

你可能会想到 ceil(17.0/5)。能算对,但浮点数在大数时会有精度误差,ceil 偶尔会差 1。整数题就用整数写法,(n + per - 1) / per 永远不会有误差。

还有一种问法:「超过这个数就要多算一份」,比如虫子每 2 小时吃一个苹果,问 n 小时后剩几个。这类题的坑在于边界——正好整除时不能多算。

差一份和多一份

两题的共同点:先在纸上手算 3 组数据,确认你的公式对,再写代码。特别是「正好整除」那一组,它是唯一的失分点。

4.5 剩下的应用题

前面学的都用得上,剩下的题是把公式套进实际情境。

公式与应用

这类题的通用做法:先写出通项公式,再把题目给的量代进去。别一上来就写代码,先在纸上把公式写出来。

第 4 章验收

  • 能说出 % 10 和 / 10 各自的作用
  • 会用 x = x * 10 + d 拼数
  • 知道「能装几份」和「至少几份」是两个不同的算法
  • 有「先手算三组小数据」的习惯

5. 浮点运算与输出格式

前面全是整数。这一章引入小数,而小数带来的第一个麻烦不是计算,是输出格式。

5.1 double 与「保留几位小数」

double x = 3.14159;
printf("%f\n", x);       // 3.141590   默认 6 位
printf("%.2f\n", x);     // 3.14       保留 2 位
printf("%.0f\n", x);     // 3          不要小数

%.2f 拆开看是两截:

  • f —— 这是个浮点数
  • .2 —— 小数点后保留 2 位

printf 输出浮点数默认保留 6 位,所以要几位就写几位。保留是四舍五入的:

printf("%.1f\n", 3.45);   // 3.5
printf("%.1f\n", 3.44);   // 3.4

这只是显示上的处理,变量里存的值没变。 x 还是 3.14159,只是打印时被截了。

顺带记住另一个高频写法 %02d:宽度 2 位,不足补 0。日期时间题必用。

printf("%02d:%02d\n", 9, 5);    // 09:05

同理 %5d 是宽度 5、补空格右对齐,%-5d 是左对齐。第 12 章输出图形时会用到。

除法要注意:double 除以 int 得到 double,但两个 int 相除还是 int:

int a = 7, b = 2;
printf("%.2f\n", a / b);        // 3.00  ← 错!
printf("%.2f\n", 1.0 * a / b);   // 3.50  ← 对

第一行为什么错?因为 a / b 在赋值或输出之前就已经按整数算完了,得到 3,转成 double 也只是 3.0000。

输出格式

三题都要求特定小数位。题目说保留 2 位就写 2 位,写 3 位算错——OJ 是逐字符比的。

5.2 圆、球与 M_PI

几何题离不开圆周率。有两种写法:

const double PI = 3.14159;   // 题目给了就用题目给的
double r; scanf("%lf", &r);
printf("%.2f\n", PI * r * r);

题目给的 π 值必须照抄,不能自己写 3.14 或 3.1415926。有些题明确说「取 π = 3.14」,用了更精确的值反而错。

M_PI 是 cmath 里的常量,约 3.14159265358979。只有在题目没说 π 取多少时才用。

圆和球

球体积那条公式里 4/3 是个陷阱:写成 4/3 * PI * r*r*r 会得到 0(两个整数相除)。要写 4.0/3 或 4*PI*r*r*r/3。

5.3 三角形面积:海伦公式

已知三边求面积,用海伦公式:

p = (a + b + c) / 2
S = √(p(p-a)(p-b)(p-c))
double a, b, c;
scanf("%lf %lf %lf", &a, &b, &c);
double p = (a + b + c) / 2;
printf("%.2f\n", sqrt(p*(p-a)*(p-b)*(p-c)));

sqrt() 是开平方,在 cmath 里。注意 p 必须是 double——如果 a+b+c 是奇数,整数除法会让 p 丢掉 0.5,后面全错。

顺带一提,判断三条边能不能组成三角形,是第 10 章的内容,这里先用「题目保证能构成」的前提。

三角形面积

做完对一下:三边为 3、4、5 的三角形面积应该是 6。用这个自己验一遍代码。

5.4 生活计算:打折、利息、体重

这一组题不难,但每一题的坑都在「单位」和「取整方式」上:

  • 打折题:是打几折(乘 0.85)还是减多少钱(减 15),看清楚
  • 利息题:年利率要不要除以 100,存几年要不要乘年数
  • 换算题:英尺英寸、斤公斤,换算系数题目会给,照抄
// 打折示例:原价 x,打 y 折
double x; int y;
scanf("%lf %d", &x, &y);
printf("%.2f\n", x * y / 10);

y 折 表示原价的 y/10。八折就是 8/10 = 0.8。这里 y 是整数,y/10 会是 0,所以要先乘后除:x * y / 10。

单位换算与打折

这一组 6 题,全部做完再往下。它们看着简单,但「单位搞错」是这一章最常见的失分点。

5.5 综合

把前面学的拼起来

396 和 83 这两题纯粹考输出格式:空格在哪、逗号在哪、最后有没有换行。做完把输出和自己的预想逐字比一遍。

第 5 章验收

  • 会用 fixed << setprecision(n) 控制小数位
  • 知道两个 int 相除会丢小数,要写 1.0 * a / b 或 4.0/3
  • 题目给的 π 值会照抄,不自己发挥
  • 做生活计算题时,先确认单位再动笔

6. 字符与 ASCII

这一章换个视角:字符在计算机里其实就是整数。

6.1 char 就是一个小整数

char c = 'A';
printf("%c\n", c);        // A
printf("%d\n", c);        // 65

'A' 在内存里存的不是字母 A,是数字 65。printf("%c", c) 时它按字符解释,输出 A;(int)c 强制按整数解释,输出 65。

这张对照表你必须记住三个锚点:

字符 码值
'0' 48
'A' 65
'a' 97

其他都能推:'B' 是 66,'b' 是 98,'1' 是 49。

三个关键规律:

  • 数字字符 '0'~'9' 是连续的,所以 '7' - '0' = 7
  • 大写字母连续,'D' - 'A' = 3
  • 小写字母连续,'d' - 'a' = 3
  • 但大写和小写之间差 32,中间隔了几个符号,不连续

注意 'A' 和 "A" 是两回事:单引号是字符,双引号是字符串(第 31 章讲)。

字符的码值

第 2 题那个 +1 别漏:'A' - 'A' 是 0,但题目说的序号通常从 1 开始。

6.2 字符和数字的互转

这是初学者最容易混的一组概念,分清楚:

// 字符 '5'  →  数字 5
char c = '5';
int n = c - '0';        // 5

// 数字 5  →  字符 '5'
int x = 5;
char d = x + '0';       // '5'

为什么要减 '0'? 因为 '5' 的码值是 53,直接 (int)'5' 得到的是 53 不是 5。减去 '0'(48)才是真正的数字 5。

反过来,5 + '0' = 53,正好是 '5' 的码值。

同理,数字转字母:1 + 'A' - 1 或 x - 1 + 'A',具体看题目是 0 起还是 1 起。

字符与数字互转

第 1 题的坑:'9' 的两倍是 18,不是一位数了,题目可能要求特殊处理。

6.3 在字母表上移动

凯撒密码、字母邻居、字母距离,本质都是同一件事:把字符转成编号,做加减,再转回字符。

char c = 'x';
char d = c + 3;                 // '{' ← 错了,跑到字母表外面
char e = (c - 'a' + 3) % 26 + 'a';   // 'a' ← 对,转回来了

% 26 是关键。字母表是环形的,z 后面接 a,用取模就能实现「转一圈回来」。

写这类题的统一套路:

  1. 字符减 'a'(或 'A'),变成 0~25 的编号
  2. 编号做加减,需要回绕就 % 26
  3. 加回 'a'(或 'A'),变回字符
char shift(char c, int k) {
    return (c - 'a' + k % 26 + 26) % 26 + 'a';
}

那个 + 26 是为了处理 k 为负数的情况——C++ 里负数取模结果是负的,(-3) % 26 是 -3 不是 23。加上 26 再取模就正常了。(这段是个函数,第 27 章会讲。)

字母表上的移动

这四题做完,你就掌握了字符类题目的全部套路。每一题都先在本子上列出 3 个输入输出的对照,再写代码。

第 6 章验收

  • 记得住 '0'=48、'A'=65、'a'=97
  • 知道 '5' - '0' 是 5,(int)'5' 是 53
  • 会用 % 26 实现字母表回绕
  • 知道负数取模要先 + 26

7. 综合应用

这一章四道题,知识点都在前面出现过,考的是能不能组合起来用。

7.1 取模让数字循环

星期几、时钟、环形队列,都是「到顶了回到开头」:

// 今天是星期 d(1~7),问 n 天后是星期几
int ans = (d - 1 + n) % 7 + 1;

先减 1 变成 0~6,加 n,取模 7 绕回来,再加 1 变回 1~7。

为什么不能直接 (d + n) % 7? 因为那样 7 会变成 0,而星期没有 0。减 1 加 1 就是为了把 1~7 映射到 0~6 再映射回来。

日期里的取模

7.2 位权与进制

152 这个数,其实是 1×10² + 5×10¹ + 2×10⁰。每一位乘的 10 的幂,叫「位权」。

换进制就把 10 换成别的:

// 二进制 1011 转十进制
int b = 1011;
int ans = 0, w = 1;
while (b > 0) {
    ans += (b % 10) * w;   // 当前位 × 位权
    w *= 2;                // 位权翻倍
    b /= 10;
}
// ans = 11

这是「按位拆分」和「乘方」的组合,两个都在前面学过。

进制与幂

  • BCD数转换修复 —— BCD 是「每四位二进制存一个十进制位」,本质是拆位重拼
  • 计算 2 的幂 —— 注意题目可能要求大数或特定位

390 如果用 pow(2, n) 要注意:pow 返回 double,n 大了会丢精度。整数幂就用循环乘:

int p = 1;
for (int i = 0; i < n; i++) p *= 2;

7.3 时间的换算

时、分、秒之间的换算是 60 进制,跟前面 10 进制的思路一样,只是除数是 60:

int total;          // 总秒数
scanf("%d", &total);
int h = total / 3600;
int m = total % 3600 / 60;
int s = total % 60;

注意第二行:total % 3600 先去掉整小时,剩下的再 /60 得到分钟。

时间计算

如果涉及跨天,注意小时不要出现负数,需要 +24 再 %24。

第 7 章验收

五章 49 题全部做完,你应该达到这个状态:

  • 拿到任何「读入几个数、算一个结果」的题,10 分钟内能写出能过样例的代码
  • 看到除法会先问:要不要小数?向上还是向下取整?
  • 看到小数输出会先找题目要求保留几位
  • 看到字符会本能地想「它的码值是多少」

如果这四件事还做不到,不要急着进第 8 章。 判断和循环的题会把计算当工具用,工具不熟,后面每一步都会慢半拍。

延伸阅读

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

  • 可以先看:变量 —— 类型系统全貌。只看开头「基础类型」那部分,后面讲 _Bool、C23 变化的跟你没关系
  • 可以先看:运算 —— 整数除法和取模的规则,这一章天天用

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

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

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

results matching ""

    No results matching ""