例题给全套:题面、样例、参考代码、以及一步步的讲解。先自己读题想一遍, 再看代码,最后对着讲解核一遍自己想的对不对。
练习只给题面、样例和一句提示,答案是折叠起来的 —— 请先自己写一版跑通了再展开。写不出来也别直接看答案,先回速查那一组找找。
每份代码都能直接点运行:输入框里已经填好了样例输入, 把它改一改再跑,是这一页最值钱的用法。
先把地基摆一遍
每个整数类型都有一个固定的抽屉大小。装不下的时候,C++ 不会报错、不会提醒,它只是把超出去的部分扔掉,留下一个看起来很正常的错数字 —— 这叫溢出。
★ 记两个数量级就够:int 大约 2.1 × 10⁹(二十一亿),long long 大约 9.2 × 10¹⁸。竞赛题面里常写 1 ≤ aᵢ ≤ 10⁹,单个数 int 装得下 —— 可两个这样的数一乘就是 10¹⁸,当场爆掉。
⇒ ★★ 所以动笔之前有一个三十秒的动作:拿题面的上限乘一遍,看最大的那个中间值有多大。不是看输入多大,也不是看答案多大,是看算的过程中出现过的最大的那个数。本书里栽的每一次都栽在这儿。
★ 拿不准就一律 long long,代价几乎为零。只有「开一个几千万个元素的大数组」时,才值得回头精打细算 —— 那时内存翻一倍是真的会 MLE。
例题(2 道)
每道都给全套:题面 → 样例 → 参考代码 → 一步步的讲解。
例题 ① 最大的那个乘积
给 n 个正整数,请从中选两个不同位置的数相乘,求乘积的最大值。
n(2 ≤ n ≤ 1000);第二行 n 个整数 aᵢ(1 ≤ aᵢ ≤ 10⁹)。输入
3 100000 99999 2
输出
9999900000
int 上限的 4.6 倍了。两层循环谁都会写。动笔之前先做那个三十秒的动作:把题面的上限乘一遍,看算的过程中出现过的最大的那个数有多大。
// 例题 ①「最大的那个乘积」—— 整数类型组//// 这道题的关卡一个字都不在算法里:两层循环谁都会写,// 会挂的是**中间那次乘法**。动笔之前先乘一遍:// 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;}点「运行 ▶」看结果
- 先算账:
aᵢ最大 10⁹,两个 10⁹ 相乘是 10¹⁸。而int只到 2.1 × 10⁹、long long到 9.2 × 10¹⁸ ⇒ 乘积必须用long long装。 - ⚠ 可光把结果声明成
long long没用:long long p = a[i] * a[j];里等号右边那次乘法两边都是 int,于是它就是一次 int 乘法 —— 溢出在赋值之前就已经发生了,抄给谁都救不回来。 - 正确写法是
(long long)a[i] * a[j]:把其中一个转成 long long,整个表达式就按 long long 算。⇒ 两个 int 相乘之前先转一个,这是一条无脑规则。 - 至于
a数组本身 —— 单个数 10⁹ 在int里装得下,所以它用int完全没问题。⇒ 「要不要 long long」的主语从来不是变量,是那一次运算。
本书在第 11、19、23、35、43 章各栽过一次,栽的都是这一处:看的不是输入多大,也不是答案多大,是算的过程中出现过的最大的那个数。
用到的写法:int long long (long long)a * b
例题 ② 负数的除法和取模
给一个正整数 p 和 n 个整数 a(可能是负数)。对每个 a,输出三个数:a / p、a % p,以及调整到 [0, p) 之后的余数。
⚠ C++ 的整数除法是「向零取整」:-7 / 2 得 −3(不是 −4),于是 -7 % 2 得 −1 —— 取模的符号跟着被除数走。
p 和 n(1 ≤ 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。
// 例题 ②「负数的除法和取模」—— 整数类型组//// 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;}点「运行 ▶」看结果
- 「向零取整」就是把小数部分直接砍掉:−2.33 砍成 −2,而不是往下取到 −3。
- 砍完之后这个等式仍然成立:
(a / p) * p + (a % p) == a。⇒ 商往零的方向少了一点,余数就只好是负的,来把账补平。 - 要一个非负的余数,写
(a % p + p) % p:先加一个p把它抬成正的(a % p最小是−(p−1),加一个 p 之后一定 ≥ 1),再取一次模把可能多出来的那个 p 去掉。 - ⚠ 为什么非要这么写:第 42、43 章的答案要求落在
[0, p)里。直接打出一个负数就是 WA,而这件事样例常常测不出来 —— 样例里的数往往全是正的。
这是全书唯一一处「C++ 的规矩和小学数学不一样」的地方,而它一年到头只咬人一次 —— 就在取模题里。
练习(3 道)
答案折叠着,先自己写一版。
练习 ① 求和
给 n 个整数,求它们的和。
n(1 ≤ n ≤ 10⁵);第二行 n 个整数 aᵢ(|aᵢ| ≤ 10⁹)。输入
5 1000000000 1000000000 1000000000 -1 2
输出
3000000001
int 里,加起来就不在了。每个数都装得进 int。那么「和」呢?先乘一遍再动笔。
练习 ② 1 加到 n
给一个正整数 n,求 1 + 2 + … + n。
⚠ n 可以到 10⁹ ⇒ 一个一个加是来不及的,要用公式 n(n+1)/2。
n(1 ≤ n ≤ 10⁹)。输入
1000000000
输出
500000000500000000
n = 10⁹ 时答案约 5 × 10¹⁷ —— 装得进 long long,但中间那次乘法要小心。答案装得进 long long 没问题。可 n * (n + 1) 这次乘法是什么类型的乘法?(回头看例题 ① 的第二步。)
顺带一个能自己想明白的小问题:为什么先乘再除一定除得尽?(n 和 n+1 里必有一个是偶数。)
用到的写法:(long long)a * b long long
练习 ③ 相邻两数差的最大值
给 n 个整数,求相邻两个数之差的绝对值的最大值。
⚠ 如果 n = 1,那就一对相邻的数都没有,请输出 0。
n(1 ≤ n ≤ 10⁵);第二行 n 个整数。输入
5 3 1 4 1 5
输出
4
输入
1 7
输出
0
n = 1。很多人第一版会写 for (int i = 0; i < a.size() - 1; i++) —— 跑一下看看会怎样。题目本身两行就写完了。坑全在 n = 1 上:a.size() 是无符号的,0 - 1 不是 −1。⇒ 想想有没有一种写法根本不用做那个减法。
size() 是无符号这件事,本书踩过不止一次。两个改法:(int)a.size() - 1,或者干脆写成 i + 1 < n —— 后者更稳,因为它不做减法。
用到的写法:v.size()