课程 · C++ 速查 · 整数类型与溢出

整数类型与溢出 例题与练习

把「动笔之前先拿题面的上限乘一遍」变成一个真的会做的动作 —— 这一组的每道题都在那次乘法上。

2 道例题3 道练习5 份可运行代码回速查看这一组的 6 条 →
怎么用这一页

例题给全套:题面、样例、参考代码、以及一步步的讲解。先自己读题想一遍, 再看代码,最后对着讲解核一遍自己想的对不对。

练习只给题面、样例和一句提示,答案是折叠起来的 —— 请先自己写一版跑通了再展开。写不出来也别直接看答案,先回速查那一组找找。

每份代码都能直接点运行:输入框里已经填好了样例输入, 把它改一改再跑,是这一页最值钱的用法。

先把地基摆一遍

4 段

每个整数类型都有一个固定的抽屉大小。装不下的时候,C++ 不会报错、不会提醒,它只是把超出去的部分扔掉,留下一个看起来很正常的错数字 —— 这叫溢出。

★ 记两个数量级就够:int 大约 2.1 × 10⁹(二十一亿),long long 大约 9.2 × 10¹⁸。竞赛题面里常写 1 ≤ aᵢ ≤ 10⁹,单个数 int 装得下 —— 可两个这样的数一乘就是 10¹⁸,当场爆掉

⇒ ★★ 所以动笔之前有一个三十秒的动作:拿题面的上限乘一遍,看最大的那个中间值有多大。不是看输入多大,也不是看答案多大,是看算的过程中出现过的最大的那个数。本书里栽的每一次都栽在这儿。

★ 拿不准就一律 long long,代价几乎为零。只有「开一个几千万个元素的大数组」时,才值得回头精打细算 —— 那时内存翻一倍是真的会 MLE。

例题(2 道)

每道都给全套:题面 → 样例 → 参考代码 → 一步步的讲解。

例题 ① 最大的那个乘积

intEx1.cpp

n 个正整数,请从中选两个不同位置的数相乘,求乘积的最大值。

输入格式第一行一个整数 n2 ≤ n ≤ 1000);第二行 n 个整数 aᵢ1 ≤ aᵢ ≤ 10⁹)。
输出格式一行一个整数,表示最大的乘积。

输入

3
100000 99999 2

输出

9999900000
最大的两个数是 100000 和 99999,乘积 9 999 900 000 —— 已经是 int 上限的 4.6 倍了。
先想一想

两层循环谁都会写。动笔之前先做那个三十秒的动作:把题面的上限乘一遍,看算的过程中出现过的最大的那个数有多大。

intEx1.cpp参考代码
// 例题 ①「最大的那个乘积」—— 整数类型组
//
// 这道题的关卡一个字都不在算法里:两层循环谁都会写,
// 会挂的是**中间那次乘法**。动笔之前先乘一遍:
// a 最大 10^9,两个 10^9 相乘是 10^18 —— int 装到 2.1 × 10^9 就满了。
#include <bits/stdc++.h>
using namespace std;
int main() {
int n;
cin >> n;
vector<int> a(n); // 单个数 10^9 在 int 里装得下,所以这里用 int 没问题
for (int i = 0; i < n; i++) cin >> a[i];
long long best = 0;
for (int i = 0; i < n; i++)
for (int j = i + 1; j < n; j++) {
// ⚠ 关键就是这个 (long long):
// 写成 a[i] * a[j] 的话,这**一次乘法本身**就是 int 乘法,
// 溢出在赋值之前就已经发生了,外面套多少个 long long 都救不回来。
long long p = (long long)a[i] * a[j];
best = max(best, p);
}
cout << best << "\n";
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
这段代码是怎么想出来的
  1. 先算账:aᵢ 最大 10⁹,两个 10⁹ 相乘是 10¹⁸。而 int 只到 2.1 × 10⁹、long long 到 9.2 × 10¹⁸ ⇒ 乘积必须用 long long 装。
  2. ⚠ 可光把结果声明成 long long 没用long long p = a[i] * a[j]; 里等号右边那次乘法两边都是 int,于是它就是一次 int 乘法 —— 溢出在赋值之前就已经发生了,抄给谁都救不回来。
  3. 正确写法是 (long long)a[i] * a[j]:把其中一个转成 long long,整个表达式就按 long long 算。⇒ 两个 int 相乘之前先转一个,这是一条无脑规则。
  4. 至于 a 数组本身 —— 单个数 10⁹ 在 int 里装得下,所以它用 int 完全没问题。⇒ 「要不要 long long」的主语从来不是变量,是那一次运算。
记住这一句

本书在第 11、19、23、35、43 章各栽过一次,栽的都是这一处:看的不是输入多大,也不是答案多大,是算的过程中出现过的最大的那个数。

用到的写法:int long long (long long)a * b

例题 ② 负数的除法和取模

intEx2.cpp

给一个正整数 pn 个整数 a(可能是负数)。对每个 a,输出三个数:a / pa % p,以及调整到 [0, p) 之后的余数。

⚠ C++ 的整数除法是「向零取整」:-7 / 2−3(不是 −4),于是 -7 % 2−1 —— 取模的符号跟着被除数走。

输入格式第一行两个整数 pn1 ≤ p ≤ 10⁹);第二行 n 个整数。
输出格式n 行,每行三个整数。

输入

3 4
-7 7 -1 0

输出

-2 -1 2
2 1 1
0 -1 2
0 0 0
-7 / 3 = −2(−2.33 砍掉小数部分,砍向 0),-7 % 3 = −1,调整后是 2。而 0 那一行三个都是 0。
先想一想

先自己在纸上算一遍 -7 / 3-7 % 3,再跑一遍看对不对 ——大多数人第一次都会把它们算成 −3 和 2。

intEx2.cpp参考代码
// 例题 ②「负数的除法和取模」—— 整数类型组
//
// C++ 的整数除法是「向零取整」:-7 / 2 得 -3(不是 -4)。
// 于是 -7 % 2 得 -1 —— **取模的符号跟着被除数走**。
// 第 42、43 章要求答案非负,所以那两章一律写 (a % p + p) % p。
#include <bits/stdc++.h>
using namespace std;
int main() {
int p, n;
cin >> p >> n;
for (int t = 0; t < n; t++) {
int a;
cin >> a;
int q = a / p; // 向零取整:小数部分直接砍掉
int r = a % p; // 可能是负的
int ok = (a % p + p) % p; // 调整成 [0, p) 里的那个代表
cout << q << ' ' << r << ' ' << ok << "\n";
// 砍完之后这个等式仍然成立:q * p + r == a
}
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
这段代码是怎么想出来的
  1. 「向零取整」就是把小数部分直接砍掉:−2.33 砍成 −2,而不是往下取到 −3。
  2. 砍完之后这个等式仍然成立:(a / p) * p + (a % p) == a。⇒ 商往零的方向少了一点,余数就只好是负的,来把账补平。
  3. 要一个非负的余数,写 (a % p + p) % p:先加一个 p 把它抬成正的(a % p 最小是 −(p−1),加一个 p 之后一定 ≥ 1),再取一次模把可能多出来的那个 p 去掉。
  4. ⚠ 为什么非要这么写:第 42、43 章的答案要求落在 [0, p) 里。直接打出一个负数就是 WA,而这件事样例常常测不出来 —— 样例里的数往往全是正的。
记住这一句

这是全书唯一一处「C++ 的规矩和小学数学不一样」的地方,而它一年到头只咬人一次 —— 就在取模题里。

用到的写法:-7 / 2 int

练习(3 道)

答案折叠着,先自己写一版。

练习 ① 求和

intTry1.cpp

n 个整数,求它们的和。

输入格式第一行一个整数 n1 ≤ n ≤ 10⁵);第二行 n 个整数 aᵢ|aᵢ| ≤ 10⁹)。
输出格式一行一个整数。

输入

5
1000000000 1000000000 1000000000 -1 2

输出

3000000001
三个 10⁹ 加起来就是 3 × 10⁹ —— 单个数都在 int 里,加起来就不在了。
提示

每个数都装得进 int。那么「和」呢?先乘一遍再动笔。

intTry1.cpp参考答案

用到的写法:long long int

练习 ② 1 加到 n

intTry2.cpp

给一个正整数 n,求 1 + 2 + … + n

n 可以到 10⁹ ⇒ 一个一个加是来不及的,要用公式 n(n+1)/2

输入格式一行一个正整数 n1 ≤ n ≤ 10⁹)。
输出格式一行一个整数。

输入

1000000000

输出

500000000500000000
n = 10⁹ 时答案约 5 × 10¹⁷ —— 装得进 long long,但中间那次乘法要小心。
提示

答案装得进 long long 没问题。可 n * (n + 1) 这次乘法是什么类型的乘法?(回头看例题 ① 的第二步。)

intTry2.cpp参考答案
记住这一句

顺带一个能自己想明白的小问题:为什么先乘再除一定除得尽?nn+1 里必有一个是偶数。)

用到的写法:(long long)a * b long long

练习 ③ 相邻两数差的最大值

intTry3.cpp

n 个整数,求相邻两个数之差的绝对值的最大值。

⚠ 如果 n = 1,那就一对相邻的数都没有,请输出 0。

输入格式第一行一个整数 n1 ≤ n ≤ 10⁵);第二行 n 个整数。
输出格式一行一个整数。

输入

5
3 1 4 1 5

输出

4
相邻四对的差是 −2、3、−3、4,绝对值最大的是 4。

输入

1
7

输出

0
⚠ 这一组才是这道题真正的考点:n = 1。很多人第一版会写 for (int i = 0; i < a.size() - 1; i++) —— 跑一下看看会怎样。
提示

题目本身两行就写完了。坑全在 n = 1 上:a.size()无符号的,0 - 1 不是 −1。⇒ 想想有没有一种写法根本不用做那个减法

intTry3.cpp参考答案
记住这一句

size() 是无符号这件事,本书踩过不止一次。两个改法:(int)a.size() - 1,或者干脆写成 i + 1 < n —— 后者更稳,因为它不做减法。

用到的写法:v.size()