算法竞赛中的 C++ 语法操作
前言
昨天在某群里看到有人问 「有没有那种总结好的语法技巧啊」。突然想到自己也没有做过类似的总结,于是就有了这篇文章。需要注意的是,本文仅列出了一些常见技巧,欢迎指正以及补充!
本文的写作顺序_并不总_符合认知的先后,如果有任何疑问,请善用搜索引擎或 Ctrl-F 查找该内容。
算法竞赛代码与工程代码目的与标准并不完全相同,请勿以工程角度看待此文章中的若干「技巧」。同时笔者也需要指出,许多算法竞赛的习惯并不适用于工程。
- 用
!标记了最近新增的条目。 - 用
*标记了最近修改的条目。 - 用
~标记了「这其实是一个_坏_习惯,但算法竞赛就这样用吧」。 - 用
^标记了准备删除的条目,笔者将在下次更新中移除这些条目。
作者是个老鸽子,乐观情况下_平均_一个月维护一次。另,本文有搬迁至 github 的想法,届时文章将会更加精美全面,敬请期待。
(2023-04-10)假的,可能等不到了,现附上 Markdown,读者可随意使用,欢迎接力。
预处理 1
1
cppreference 预处理器 https://en.cppreference.com/w/cpp/preprocessor
~ 万能头 2 及其预编译 —— #include
2
为什么你不应该使用万能头 https://stackoverflow.com/questions/31816095/why-should-i-not-include-bits-stdc-h
要使用万能头,请使用 g++ 作为编译器。
1 | #include <bits/stdc++.h> |
另,万能头本身十分庞大,预编译是节省编译时间的重要手段,具体方法如下:
- 找到本机
bits/stdc++.h,笔者该文件位置在/usr/include/c++/12.1.1/x86_64-pc-linux-gnu/bits/stdc++.h - 运行
g++ --yourflags bits/stdc++.h,其中--yourflags代表你喜好的参数 - 下次编译也使用第 2 步提到的指令编译。为节省生命,建议写入
CMakeLists.txt
文本替换宏 3 —— #define / #undef / 预定义替换宏
3
https://en.cppreference.com/w/cpp/preprocessor/replace
许多选手喜欢定义若干宏,读者要决定是否喜欢这些大可自行尝试。在此笔者列出一些较为常见的文本替换宏:
1 | #define int long long // 可能报告为错误(事实上确实不应这样做) |
使用 #undef
取消文本替换宏,意义为「仅在包裹的代码段落中执行替换」 。
C++ 预定义了一些宏,它们会随着「情况」的不同发生改变。如
__cplusplus 指代当前语言标准, __LINE__
将指代当前行,__FUNC__
将指代当前所在的函数。此处不作展开,详情请参见 cppreference
预定义宏。配合这一点,可以写出一个简单的调试文本替换宏:
1 | #define here log("Passing [%s] in LINE %d\n", __FUNCTION__, __LINE__) |
条件宏 4
4
https://en.cppreference.com/w/cpp/preprocessor/conditional
#ifdef, #else, #endif
之类的宏操作会在编译过程中选择某些部分编译与否等等。
在竞赛中最经典的使用是配合文件流重定向,在本机使用文件输入,而在 OJ
上使用标准读入。大部分 OJ 都定义了
ONLINE_JUDGE 的宏,因此可以借此作为开关。
1 | #ifndef ONLINE_JUDGE |
编译时附加 -DLOCAL 来定义一个名为 LOCAL
的宏。
断言
这并非一个函数,而是一个宏。接受一个表达式,若为假程序将直接退出,_十分_便于调试。
例如,某区域是你永不希望到达的,你可以在此区域附加
assert(false)。也可使用
__builtin_unreachable() / std::unreachable()5 来标记不可达的区域。
5
https://en.cppreference.com/w/cpp/utility/unreachable
有的 OJ 会屏蔽 assert (事实上宏 NDEBUG
决定之),不过实现一个 6 也很容易:
6
这个宏参考了 f0re1gners 的模板库 https://f0re1gners.github.io/template/template.pdf
1 | #ifdef ONLINE_JUDGE |
!~ 命名空间 7
7
https://en.cppreference.com/w/cpp/language/namespace
namespace 是 C++ 中十分有效的解决命名冲突的手段。
假设现在要处理两段命名冲突的数据,将其命名为类似的名称似乎并没有想象中的美观。这种情况下,可以借助
namespace 将其包裹住。
1 | int _hash[maxn], _hash_2[maxn]; |
使用时:L::_hash[i], R::_hash[i] 。
欲指代全局的命名空间(如 C 库函数),可直接使用 :: 打头,如 ::abs(a)。
匿名的空间也是可行的,对于 namespace {int xxx;} 的访问可直接 ::xxx。
C++ 标准库函数都位于 std 中,这意味着若要使用他们,则需
std::xxx 指代它们。不少竞赛选手常常喜欢使用一句多为人诟病8 的 using namespace std,这句话将在全局引用
std 命名空间。
8
https://stackoverflow.com/questions/1452721/why-is-using-namespace-std-considered-bad-practice
如果把 ranges::views::iota_view 比如文件路径
/home/patricky/Downloads/files,那么
using namespace
的意义就如同将其置于环境变量中,意味着你可随时随地使用其下的文件。使用此方法之后,你应当更小心地使用变量名。
另外一种不错的手段是,使用时再 using。如
using std::cout 之后即可
cout << "Anything you want." 了。
说起来,如此长的命名空间
std::ranges:views(尽管它已被别名为
std::views)
实在太不方便了,用下面的语法来给它起个别名:
1 | namespace NV = std::ranges::views; |
作用域 -- 另一个解决命名冲突的方法
一个花括号围住的内容。
1 | int n = 1; |
运算符的重载 9
9
https://en.cppreference.com/w/cpp/language/operators
简单说来就是将某函数绑定到某运算符上(优先级与之前一致),常见的如定义一个_不小 10_ 的矩阵:
10
小矩阵请直接使用 array<array<int, N>, N>
1 | template<class T = int> |
这样之后就可以对 Matrix m(2, 2) 使用
m[1][0] 了。
写一个比较器(通常是重载小于号)再传入 STL 容器中,这种时候,函数的头就十分重要:
1 | struct node { |
仿函数 / 函子
重载括号之后,使用括号运算符的感觉就像是函数一样。
如果想用
unordered_{, multi}{map, set}<pair<T1, T2>, T3>,可以传一个仿函数进去,如
unordered_map<pair<int,int>,int, your_hash>
或直接特化 hash:
1 | template<class T1, class T2> |
不过其实不如写一个函数将 pair<int, int>
先哈希一次,再用这个数字来哈希,详情请见:
Blowing up unordered_map, and how to stop getting hacked on it - Codeforces
* Lambda 表达式 / 匿名函数 / 闭包 11
11
https://en.cppreference.com/w/cpp/language/lambda
简单来说即「函数中的函数」,然而其类型两两不同(哪怕写的一模一样)。一个
lambda 表达式包含如下几个部分:
1 | [] // 捕获列表,包含两种捕获方式:按值(const)/引用。将会拷贝一份至 lambda 对应的类中。 |
因此_最简单的_ lambda
应当是:[]{}。为便于多次调用,可以使用 auto
自动推导其类型:
1 | auto sayHell = [] { cout << "Hell World!"; }; |
来看标准库在后续版本对 lambda 表达式做的一些加强:
- C++14 起:
lambda表达式可以是模板了,但只能通过在类型中写auto。
1 | auto foo = [](auto cmp, auto a, auto b) { |
- C++14 起:带有捕获初始化的
lambda,可以在捕获的时候给初值了。
1 | // x ++ (错误,必须带等号) |
这使得 bind 的使用场景越来越少。
- 今天的
lambda:若干this/*this,支持<>定义模板lambda了等等,此处从略。
来写个递归的 lambda (尽管 C++23 可以
this auto self 了):
1 | auto fib = [](auto &&self, int n) -> int { |
配合上述 C++14 特性,可以写一个即刻调用的版本:
1 | [&, fib{[&](auto &&self, int n) -> int { |
这样写_有点_麻烦,这主要是因为写下 fib
这名字时类型未定,如果有个什么工具标注一下类型就_好_了,有请 ——
函数对象 function
12
https://en.cppreference.com/w/cpp/named_req/Callable 13: https://en.cppreference.com/w/cpp/utility/functional
对于上面的例子,依然需要在定义时给出类型以及返回值:
1 | function<int(int)> fib = [&fib](int n) -> int { |
其他时候的 function 就自由得多。下面的例子演示了如何使用
function 存储单个参数的三个函数。
1 | #include <functional> |
折叠表达式 14
14
https://en.cppreference.com/w/cpp/language/fold
要使用这一特性,请使用 C++17 及以上语言版本。
此处不展开了,本身是个_不太复杂_语法。来看两个笔者常用的简单功能:
输出 tuple
apply(a, t) 将会把 t
拆包作为「可运行的」a
的参数,另外逗号表达式严格自左向右执行,算是这段代码中最有技巧性的部分。
1 | template <typename... Ts> |
输出调试信息
为了利用众所周知的 cerr15 与
clog16,来写一个函数:
15
https://en.cppreference.com/w/cpp/io/cerr 16: https://en.cppreference.com/w/cpp/io/clog
1 | template <typename... Args> |
其中包含 C++17 引入的 [[maybe_unused]]
能让编译器对没有使用的参数「闭嘴」。
什么?你更喜欢古老的 printf
系列?请用(略去了条件编译):
1 | #define log(fmt, args...) fprintf(stderr, fmt, ##args) |
节省或浪费生命的小玩意
这一部分将介绍一些笔者认为十分方便的语法、工具。
超短的小语法、位运算等
1 | exit(0) // 在任意位置结束程序,然后去世 |
一些常用的循环
枚举所有状态
O(n×2n)
1 | for (int i = 0; i < 1 << n; ++i) { |
枚举子集
O(2popcount(n))
1 | for (auto i = s; i; --i &= s) { } // 从大到小 |
枚举 n 个选 k 个的集合
(kn)
1 | for (int t = (1 << k) - 1, x, y; t < 1 << n; t = ((t & ~y) / x >> 1) | y) { |
枚举倍数
O(logN)
1 | for (int j = i + i; j <= N; j += i) { } |
输出变量的类型
一个_看似很有用的_东西。
1 | #include <cxxabi.h> |
y 组合子 —— 即刻使用的递归 lambda
选自 A Proposal to Add Y Combinator to the Standard Library 17。
17
"这段代码具体什么意思?" https://riptutorial.com/cplusplus/example/8508/recursive-lambdas
1 | template <class Fun> struct y_combinator_result { |
使用案例:
1 | cout << y_combinator([&](auto &&gcd, int a, int b) -> int { |
或者多次使用:
1 | auto gcd = y_combinator([&](auto &&gcd, int a, int b) -> int { |
上述案例仅用于了解如何使用该语法。实际操作请使用
std::__gcd 或 std::gcd(C++17)。
高维 vector
尽管根据 CTAD,C++17 开始可使用
vector graph(n, vector(n, 0)) 来定义一个 n×n 的
vector,但当维数更多时,这种方法依然_不够_简洁。在 C++23
还未普及之前,下面的方法18 也相当不错:
18
这方法取自 https://codeforces.com/blog/entry/76149?#comment-606512
1 | template <class... Args> |
1 | // 维数越多越明显,但一般也就四维撑死了~ |
上面这个 x 展开是什么类型 19?无需在意。总之,_都_让编译器忙活去吧!
19
语法项
范围 for 20
20
https://en.cppreference.com/w/cpp/language/range-for
如,遍历一个 set<int> 在 C++11
之前使用迭代器:
1 | static const int a[] = {1, 2, 3, 4, 5}; |
在 C++11 使用 for (int i : s) 即可。在 C++20 还可在
for 中再定义一些变量:
1 | for (auto S = "Hi!"; const auto i : s) { } |
if / switch 中定义变量 21
21
https://en.cppreference.com/w/cpp/language/if
要使用这一特性,请使用 C++17 及以上语言版本。
从语义上来说,确实更方便了,但也许会部分编码者抓狂。
1 | if (int op = read(); op == 1) { |
auto22, 初始化 23, CTAD24, 结构化绑定 25 等
22
https://en.cppreference.com/w/cpp/keyword/auto 23: https://en.cppreference.com/w/cpp/language/list_initialization 24: https://en.cppreference.com/w/cpp/language/class_template_argument_deduction 25: https://en.cppreference.com/w/cpp/language/structured_binding
C++11 的到来使得这个语言充满活力,尤其是 auto
的拓展。聚合(或其他)初始化则更加配合这一点,一些经典的例子是:
1 | auto /*占位*/ do_nothing = []{}; // 承载 lambda |
配合后续版本增加的特性,auto 更为通用:
结构化绑定
要使用这一特性,请使用 C++17 及以上语言版本。
绑定数组、tuple-like、或者只是一个普通的结构体:
1 | static int arr[] = { 1, 2, 3, 4 }; |
CTAD
要使用这一特性,请使用 C++17 及以上语言版本。
再也不需要写全类型了,如
pair<int, double> t { 1, 1. } 可直接写为
pair t{ 1, 1. } 26
26
1. 是 double 类型而 1.F 是
float 类型
而 vector<vector<int>> a(n) 也可写为
vector a(n, vector(0, 0))
1 | for (auto [dx, dy] : {pair{0, 1}, {0, -1}, {1, 0}, {-1, 0}}) { |
输入一行带空格的字符串 27
27
https://en.cppreference.com/w/cpp/string/basic_string/getline
对一个 char[] 可使用正则表达式
scanf(" %[^\n]", str)。28
28
https://en.cppreference.com/w/c/io/fscanf
1 | string s; |
注意会吃掉 '\n' 。读者可尝试以下代码:
1 | int foo; |
多个字符串黏起来的字符串
当你的代码中出现了一行很长的字符串,你可以将它们分隔开来,就像这样:
1 | const char *str = "Hey! " |
原始字符串 29
29
https://en.cppreference.com/w/cpp/language/string_literal
不处理转义字符的字符串。
1 | string raw = R"(\n\r\b\t\ |
一些好用的 STL 类 30/ 函数
30
为什么没有 pbds? 我实在是太懒了 ...
排序、乱序等
排序
1 | sort(A.begin(), A.end()); |
已经有序
1 | is_sorted(A.begin(), A.end()); // 非严格升序 |
乱序
自 C++17 起 random_shuffle 被弃用,现在使用
shuffle 。
1 | vector a {-6, 1, 2, 3, 4, 4, 5}; |
顺便看看 stable_sort
创建一个排列
1 | iota(A.begin(), A.end(), 1); // 1, 2, 3, ... |
累积 accumulate
求和 / 积、或者是其他操作,下面是两个例子:
1 | accumulate(A.begin(), A.end(), 0LL); |
求最值
m{in{, max}, ax}{_element} 都可以传比较器,如
min(a, b, cmp)
有的时候是不同类型做操作,可以追加类型:min<long long>(0, b)
可以传 initializer_list,如
max({a, b, c, d, ...}) 不过这实际上也是_非常_蛋疼的一点
,意味着 min 是二义性的,想封装一个 Sparse
Table 板子的话需要注意这一点。
1 | max(max(max(a, b), c), d); |
离散化
1 | sort(A.begin(), A.end()); |
1 | sort(c + 1, c + 1 + tot); |
其他
包括但不限于
1 | count{, _if}() |
效率不是非常高的(甚至可以说很差):
1 | valarray |
* 其他注意事项
I/O
cin.tie(nullptr)->sync_with_stdio(false)。具体原因可直接百度或查看cppreference ios_base::sync_with_stdio以及cin相关文档。看完这些,读者也应同时理解了为什么endl那样「慢」。vector::iterator可以直接当作指针用。(RandomAccessIterator),比如V.end()[-1],V.rbegin()[0]。本质上就是i[x]等价于*(i + x)。知道
vector的大小,要么直接开,要么先reserve一下再{push, emplace}_backvector的resize / clear方法之后,记得shrink_to_fit。31一些没有
clear()成员方法的容器(queue,stack,...)可以decltype(cap){}.swap(cap)流放之。除
array外,所有容器的swap方法都是 O(1) 的。在栈上定义的
array开在栈上,而vector对象开在栈上,数据开在堆上。注意该不该引用(能不能用右值引用续命),能不能移动(如果可以尽可能使用
move)multi{set, map}::count的复杂度是 O( 出现次数 +log(n) ,而 C++20 的contains则是 O(logn)size_t类型应强转,或使用 C++20 的ssize()获取长度。用迭代器边遍历
set边删除迭代器时应使用it = S.erase(it)来代替it ++。也就是
1
2
3
4
5
6
7
for (auto it = s.begin(); it != s.end(); ) {
if (!judge(*it)) {
it = s.erase(it);
} else {
it ++;
}
}
set::merge(t)是直接把不同于两集合的元素从t直接薅走,意义与set::insert(t.begin(), t.end())不总一样。
31
https://en.cppreference.com/w/cpp/container/vector/shrink_to_fit
这个条目等我下次破防再更新
本文即将移除的条目
程序计时
笔者一直主观不赞成将计时嵌入至代码的行为,只是单文件、单元测试,完全可以使用系统的计时工具:
1 | time |
1 | (Measure-Command {xxx | Out-Default}).ToString() |

后记、致谢、贴贴
- 感谢 @MeteorZ 对 「判断是二的幂」 做的 「特判 0 」 补充。
- 感谢 @Lhgzbxhz
对「断言」做的「
__builtin_unreachable()」补充。 - 感谢 @阿汤
对「判断是二的幂」做的「
(x&(x-1))==0」补充。 - 感谢 @轻狂书生 对「程序计时」做的催更。
- 感谢 @soulmate
对删除「异常处理」与「
std::regex」内容的建议。 - 感谢 @n-WN 对「预定义宏」的补充。
changelog
UPD: 本文已经从「算法竞赛中常用的语法操作」重命名为 「算法竞赛中的 C++ 语法操作」。
UPD(22-09-16): 这次更新给每一个条目都加上了 cppreference 的链接(如果有)并重新规划了文章结构。
UPD(22-09-30): 移除了「一些位运算」条目。增加了 y 组合子。
UPD(23-04-10): 纠正了 C++14 lambda 相关描述,增加了一个即刻调用的递归 lambda 写法。并附上了本文的 markdown 版本。能坚持到现在真是一个奇迹,希望下次更新不会太久。