上一篇先把 ACM 学习篇的目录 画了出来。这一篇开始真正学习,但先不碰具体算法。
原因很简单:复杂度、输入输出和代码模板,是后面所有专题的基本装备。很多题还没到“不会算法”的程度,就已经死在数据范围看错、输入格式没处理好、变量类型溢出、模板写得别扭这些地方了。
一、先看数据范围
拿到一道题,不要急着写代码,先看数据范围。数据范围基本上是在告诉你:这个题允许你用多慢的算法。
比如题目给了 n <= 100,你可以考虑三重循环、区间 DP、Floyd;如果给了 n <= 2e5,大概率就不能写 O(n^2),要往排序、二分、前缀和、树状数组、线段树、图的线性遍历这些方向想。
一个粗略但很实用的判断表:
| 数据范围 | 通常能接受的复杂度 | 常见方向 |
|---|---|---|
n <= 10 | O(n!)、O(2^n) | 全排列、状态压缩、爆搜 |
n <= 20 | O(2^n * n) | 状压 DP、折半搜索 |
n <= 100 | O(n^3) | Floyd、区间 DP |
n <= 1000 | O(n^2) | 普通 DP、二维枚举 |
n <= 1e5 | O(n log n) | 排序、二分、堆、树状数组 |
n <= 1e6 | O(n) 或 O(n log n) | 线性扫描、前缀和、筛法 |
n >= 1e7 | 接近 O(n) | 极简线性算法、注意常数 |
这个表不是绝对标准,因为机器、语言、时限、常数都会影响结果。但它能帮我们建立第一反应:先从规模反推复杂度,再从复杂度反推算法。
二、复杂度不是背出来的
复杂度说白了,就是看代码大概要执行多少次核心操作。
一层循环通常是 O(n):
for (int i = 0; i < n; i++) {
// O(1)
}
两层独立循环通常是 O(n^2):
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
// O(1)
}
}
如果每次规模减半,通常会出现 log n:
while (l <= r) {
int mid = (l + r) / 2;
if (check(mid)) r = mid - 1;
else l = mid + 1;
}
学习复杂度时,不要只背“二分是 O(log n)”。更重要的是形成感觉:循环走多少次,状态有多少个,每个状态转移要算多久。
三、输入输出先写稳
ACM 题很少有交互式界面,大部分都是标准输入和标准输出。输入输出看起来简单,但格式一错,后面全白写。
C++ 里我一般先放这两行:
ios::sync_with_stdio(false);
cin.tie(nullptr);
它们的作用是让 cin / cout 更快。用了之后就不要再混用 scanf / printf,除非你很清楚自己在做什么。
常见输入格式大概有三类。
第一类:题目给定测试组数 T。
int T;
cin >> T;
while (T--) {
solve();
}
第二类:一直读到文件结束。
int a, b;
while (cin >> a >> b) {
cout << a + b << '\n';
}
第三类:先读规模,再读数组或图。
int n;
cin >> n;
vector<int> a(n);
for (int i = 0; i < n; i++) {
cin >> a[i];
}
输出也要保持克制。多组数据尽量用 '\n',不要频繁用 endl,因为 endl 会强制刷新缓冲区,通常没有必要。
四、变量类型要保守一点
算法竞赛里最常见的隐形错误之一是溢出。
int 大概能到 2.1e9。如果题目里有 1e5 个数,每个数最大 1e9,总和就可能到 1e14,这时必须用 long long。
long long sum = 0;
for (int x : a) {
sum += x;
}
我的习惯是:数组下标、计数、小范围循环可以用 int;答案、前缀和、乘法结果、距离、权值,优先考虑 long long。
还有一个容易忽略的点:两个 int 相乘,结果会先按 int 算,再赋值给 long long。所以要提前转型。
long long area = 1LL * width * height;
五、一份基础模板
模板不是为了显得专业,而是为了减少每次开始写题时的摩擦。模板越稳定,注意力越能留给题目本身。
我会先用这份基础版:
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int INF = 0x3f3f3f3f;
const ll LINF = 4e18;
const int MOD = 1e9 + 7;
void solve() {
// 每道题的核心逻辑写在这里
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int T = 1;
// 如果题目有多组数据,就打开下面两行
// cin >> T;
while (T--) {
solve();
}
return 0;
}
这份模板只保留最基础的东西:快速输入输出、常用类型、常量和 solve()。后面学到图论、线段树、并查集、字符串算法时,再逐步把专题模板加进去。
六、读题时的第一轮检查
真正做题时,我希望自己先养成一个固定动作。读完题后,不急着写代码,先检查这几件事:
- 输入有几组数据,是给
T还是读到 EOF; n、m、值域分别多大;- 答案会不会超过
int; - 要求的是最值、计数、可行性,还是具体方案;
- 有没有排序后更简单、二分后更简单、前缀和后更简单;
- 边界情况是什么,比如空、一个元素、全相等、全负数、不连通。
这几个问题看起来普通,但它们能把很多题从“没头绪”变成“有方向”。
七、这一篇先记住什么
这一篇不用记很多术语,先记住三句话就够了。
- 数据范围会提示复杂度,复杂度会提示算法方向。
- 输入输出和变量类型要先写稳,不要让低级错误挡住真正的问题。
- 模板不是答案,只是让自己更快进入思考状态。
下一篇可以进入第一个真正常用的基础工具:排序与二分。二分看起来只是查找,其实它背后更重要的是“单调性”这个思想。