「WTF」铁锅乱炖-实现篇
语言与环境
版本问题
由于 g++
的版本与现行版本不一致,库的包含关系就不完全相同。这个时候如果缺少头文件也有可能会本地通过编译,但是提交后可能会 CE。
典型例子就是 Dev-C++
上 cmath
还包含在 algorithm
里面。如果仅包含 algorithm
并且使用 cmath
内部的函数,在新版的 g++
下编译就会报错。
迷之重名
-
经典的“隐形”库函数
y0(double), y1(double), yn(int,double); j0(double), j1(double), jn(int,double)
,这几个小混蛋都在cmath
里面,存在于编译器的include
文件夹里的math.h
里面。问题是,它们几乎找不到踪迹。如果去 C++ reference 里面查你根本找不到它们。
如果在 Windows 环境下使用 9.3.0 版本的
g++
编译也不会检查出问题,但是,但是,一旦包含cmath
并且自定义了重名的变量/函数,那么在Linux
环境下编译的时候才会 CE。😓去查了一下,发现
j.()
表示的是第一类贝塞尔函数,y.()
表示的是第二类贝塞尔函数。
初始化疑云
这里集中了一些由于不当初始化而导致的编译问题。
在 Compiler Explorer 上以 -std=c++17
编译,所以汇编码应该对应的是 Intel x86-64 的 CPU。
-
经典错误一:使用大括号初始化数组:
如果是平凡地直接使用大括号括起来,全部清空,那么初始化不会出问题:
struct QwQ { // something }; int a[100000] = {}; QwQ b[100000] = {};
无论使用内置类型或自定义类型。
补充:如果在局部使用空大括号初始化(甚至有可能是在全局)都有可能导致爆炸!!!问题是一样的,可执行文件巨大。
但是,如果在大括号里面初始化了某些位置的值,那么会导致大括号被展开成完整的初始化列表(补上空白)。一个直接的表现就是编译出来的可执行文件非常大。当数组很大的时候效果尤其明显:
int a[10000000] = { 1, 2 }; // with a large *.exe appearing
-
经典错误二:错误地使用结构体初始化:如果在结构体或类的初始化中对非内置类型数组进行初始化,无论如何赋初值,如下:
struct Rubbish { int rub[1000]; Rubbish(): rub{} {} // safe operation }; struct RubbishBin { Rubbish a[1000]; RubbishBin(): a{} {} // DANGEROUS operation }; RubbishBin rb;
只要对这样的类型进行实例化,那么在汇编码中,构造函数会被完全展开。在上面的这段代码中,实际效果就是对于
a
中每个对象都调用Rubbish :: Rubbish()
。直接看一下汇编码的样子:外在的表现就是编译时间变长,但是可执行文件大小正常,嗯。
实际上,由于全局变量会自动清空(汇编码里面会有一个
.zero
的指令)所以写成:struct Rubbish { int rub[1000]; Rubbish(): rub{} {} // safe operation }; struct RubbishBin { Rubbish a[1000]; RubbishBin()/*: a{}*/ {} // Fine }; RubbishBin rb;
也可以达到清空的效果,并且不会出问题。
-
经典错误三:试图使用巨大的局部静态变量。
不知道为啥,使用
10^6
以上的int
局部静态数组(或者相当的空间)都会导致编译出来的文件巨大。最搞笑的是,在本地似乎找不出这样的锅,但是交到 OJ 上反而会出问题,比如 CF 会报告 CE。
-
消失的操作符
在 2022.04.11,我遇到了一个神奇的问题。
struct Unit {
int a, b;
Unit(): a( 0 ), b( 0 ) {}
Unit( int B ): a( 0 ), b( B % mod ) {}
Unit( int A, int B ): a( A % mod ), b( B % mod ) {}
inline operator bool() const {
return a || b;
}
};
inline int Qkpow( int, int );
inline int Inv( const int &a ) { return Qkpow( a, mod - 2 ); }
inline int Mul( int x, const int &v ) { return 1ll * x * v % mod; }
inline int Sub( int x, const int &v ) { return ( x -= v ) < 0 ? x + mod : x; }
inline int Add( int x, const int &v ) { return ( x += v ) >= mod ? x - mod : x; }
inline int& MulEq( int &x, const int &v ) { return x = 1ll * x * v % mod; }
inline int& SubEq( int &x, const int &v ) { return ( x -= v ) < 0 ? ( x += mod ) : x; }
inline int& AddEq( int &x, const int &v ) { return ( x += v ) >= mod ? ( x -= mod ) : x; }
inline int Qkpow( int base, int indx ) {
int ret = 1;
while( indx ) {
if( indx & 1 ) MulEq( ret, base );
MulEq( base, base ), indx >>= 1;
}
return ret;
}
inline Unit operator + ( const Unit &a, const Unit &b ) {
return Unit( Add( a.a, b.a ), Add( a.b, b.b ) );
}
inline Unit operator * ( const Unit &a, const Unit &b ) {
return Unit( Add( Mul( a.a, b.b ), Mul( a.b, b.a ) ), Mul( a.b, b.b ) );
}
inline Unit operator * ( const Unit &a, const int &b ) {
return Unit( Mul( a.a, b ), Mul( a.b, b ) );
}
inline Unit operator / ( const Unit &a, const Unit &b ) {
int inv = Inv( b.b ), q = Mul( a.b, inv );
return Unit( Mul( inv, Sub( a.a, Mul( q, b.a ) ) ), q );
}
inline Unit& operator += ( Unit &a, const Unit &b ) {
return a = a + b;
}
inline Unit& operator -= ( Unit &a, const Unit &b ) {
return a = a - b;
}
在上面这段代码中,我定义了 Unit
类型,并重载了若干个 operator
。以上能看到的就是所有重载了的 operator
。
你可以注意到,这里并没有出现 operator - ( const Unit&, const Unit& )
这样的运算符,然而在 operator -= ( Unit&, const Unit& )
的位置我们却“调用”了 operator - ( const Unit&, const Unit& )
!并且,这样的代码可以正常的过编译,一点错误都不会报。
但是调试一下,你就会发现——在执行 a - b
的时候,程序的行为实际上是先进入到 Unit :: operator bool()
里面。那么,也就不难推测,这实际上是先进行了两次类型转换,再用 bool
计算 a - b
,再构造一个 Unit
并赋值给 a
。
观察汇编码也可以得到同样的结果:
你可以看到,这里面调用了两次 Unit::operator bool() const
。
其它
-
const&
修饰符确实可以卡常,但是用起来有风险。如果传入的参数在外部被更改,那么不可避免地,函数内部的值也会被改变。外部修改并不会因为
const
修饰符而被阻止。尤其需要注意传入重复使用的全局变量的时候,尽量别用
const&
修饰。
多组不清空
-
同一变量多次重复使用,中间没有清空,直接暴毙:
int lst = N + 1; for( int i = N ; ~ i ; i -- ) { nxt[i][1] = lst; if( S[i] == '1' ) lst = i; } //这里本应该清空 lst 的 for( int i = N ; ~ i ; i -- ) { if( S[i] == '1' || S[i + 1] == '0' ) nxt[i][0] = lst; if( S[i] == '0' ) lst = i; }
-
对于判定性问题,如果我们一旦搜索到符合要求的结果就退出,那么在多次搜索的情况下,一定要注意在退出之前有没有将公共内存清空。无论是使用
return
结束语句还是使用throw
跳转都需要注意这个问题。 -
注意分治过程中,公共数组的清空,特别是在改写法的时候,不要忘了加上被修改结构的清空过程。
-
「CF505E」Mr. Kitayuta vs. Bamboos
提醒:需要重复使用的函数,尤其是可能中途退出函数过程的,一定要注意在执行函数的一开始就要将要用的东西清空!!!
这道题的
Chk()
因为中途退出的原因,中途存储用的vector
在函数体一开头没有清空,导致最终会算错东西。补充:怎么又是一模一样的问题?!在 NOI Online (2022) 也犯了。T2 找到答案之后直接退出,没有清空数组直接就爆炸了!!!
-
某道联测题目:
在转移的时候,本应在所有数据计算完之后再对不合法数据进行清理,结果边清理边计算,导致较小的不合法数据影响了之后的结果。
处理方法:注意操作的顺序,不止是在 DP 的转移中,写的时候就应该注意到顺序的问题。
-
貌似我遇到的清空问题都有一个共性:
首先,写法是在有效的运算过程中立即清空空间。
其次,其中存在一些分支,会导致程序直接退出当前计算并且跳过清空流程。
然后,在这种情况下,我几乎必然会忘记在分支出口清空数组。
结论是:要么,在所有会进行跳过的分治出口,比如
return, continue, break
处清空空间;要么,使用goto
。 -
泪目了。
DFS
的时候,用标记数组来记录有没有走过,结果重新DFS
之前搞忘清空了,直接导致我调了 2h。规避不清空的最好方法:在运行某段代码之前把所有要用到的东西全部清空一遍。
-
FFT,NTT,FWT 用到的数组,清空一定要清到可能访问的上界。覆盖式赋值不能替代清空的效果。
-
听说结构体线段树读写压力比数组线段树小。
然后一写就写错了注意多次使用线段树时,每个结点需要将所有可能用到的成员变量清空。光靠上传信息清空不了标记。
-
居然被阴了......多组没有检查所有数组,结果有的数组没有清空,就寄了。
注意边界
-
预处理应该按照值域为范围来清,结果只清到了点数范围。
-
模拟赛中出现的 typo 。
按理说应该是很常见的问题,我居然还在犯。遍历 \(n\times m\) 的平面的时候,弄混了 \(n\) 和 \(m\) 的关系,于是写错了循环边界,成功地挂分了:
for( int i = 1 ; i <= n ; i ++ ) //这里的 n 应该是 m ...
记清楚变量的含义,尤其是常见易混变量 \(n,m,x,y\) 之类的。
-
这次在预处理逆元的时候,恰好处理到了 \(2^{18}-1\) 的范围,但是实际调用时使用到了 \(2^{18}\) 的逆元,然后就爆炸了......
处理方法:一定要注意,使用任何函数/循环/数组内存时,是否会越界或超出有效范围;此外,在对复杂度影响不大时也可以适当使用快速幂/exgcd 求逆元;
-
「Gym102979L」Lights On The Road
第一次写 K 短路,结果被坑到了两个地方:
-
建图都建错了,头尾两个虚点不能连在一起,而 \(n=1\) 的时候我会连起来。这个写的时候确实没想到;
-
当 \(n=1\) 的时候,最短路树上没有非树边,这个时候堆为空。将空节点插进去做 K 短路就会 RE......
这个问题更大一些,写的时候想到了这一点,但是没有想到堆会成为空,没有判。
处理方法:注意边界情况,不要被细节卡了。
-
-
某道计数题。
数据范围明确给出:
这就说明,输入数据可以有 \(n<k\)。但是我的程序,如果不特判 \(n<k\) 就会干出奇奇怪怪的事情,导致各种 WA。
-
保证下标或指针指向的是有效的空间。
这里主要关注如何控制下标或指针不会越界,而不考虑空间开小了的情况。
例如 「模板」最小表示法,如果给定的串是
aaa...aa
的形式,则需要判断长度指针 \(k\) 是否到达了 \(|S|\),否则会访问到有效的空间之外。又注:对于下标进行运算的时候也要注意是否会越界。例如,进行减法的时候需要确认是否会越过下界,进行加法的时候需要确认是否会超出上界。尤其是下标从 0 开始计算的时候尤其容易算错!!!
-
经典错误:如果用
vector
实现多项式,那么在写加法的时候,很容易忘记边界,将任意位置的计算都写成F[i]+G[i]
。但实际上,如果i
取遍所有有效位,那么下标很有可能越界,导致 UB,返回一些乱七八糟的值。 -
阳间边界问题:左移超出边界是 Undefined Behaviour,比如不能算
1llu << 64
,不然返回什么东西谁也说不清楚。顺便补充一下,右移超出边界也是 Undefined Behaviour,所以也别写。总之,写移位的时候一定要注意参数的范围。
-
使用公共内存(或者叫“内存池”)时的问题:
-
长链剖分要注意指针移动方向不同,需要预设的空间也不同。
比如在 「POI2004」Hotel 中,\(g\) 转移时指针朝后移,所以空间得开两倍。
-
高维数组本质上是一维数组,只不过同时保存了多个指针。因此它们的低维是允许负下标的,只不过会访问到错误的值。
-
-
如果空间需要开 \(2\) 的整幂次,有可能会偷懒写成科学计数法的形式。这个时候一定要注意,实际开的空间不能小于需要的空间。
比如开 \(2^{19}\) 大小的数组,就不能近似地用
5e5
的空间替代,要开也得开5.3e5
。此外,像这种只爆了一小段空间的,有可能不会告诉你RE
,而是因为访问到了莫名的位置而得到错误答案...... -
如果有数组是倒着滚动访问的,并且还是多次使用,则一定一定要注意会不会超出清空的上界。
举个例子,多次滚动后缀和,并且每次的上界不一。这种情况下,如果滚动的上界是 \(n\),而清空的上界也是 \(n\),则会访问到错误的 \(n+1\) 号位!
-
预处理的边界:“光速幂”
也就是分块预处理幂次的算法。如果已知次数上界为 \(n\),则可以选择分块阈值 \(B=\lceil\sqrt n\rceil\)。
尤其需要注意的是,处理形如 \(a^{kB}\) 形式的幂的时候,\(k\) 应当要算到 \(B\)。因为有可能会出现 \(n=B^2\) 的情况。
-
注意所有的边界都要找齐,边界必须足够紧。
比如 「NOI2017」蚯蚓排队,由于必须只考虑跨过分割线的字符串,因此必须保证左端点不会越过分界线。如果忘了这条限制就会被卡很久!
-
真的被
vector
恶心死了。写个分治 NTT 返回
vector
就算了,中途手动清了一下 0。结果因为某一项的 \([x^1]=0\),导致最终vector
的结果比期望长度短一位,最后一个位置就莫名其妙读了一个脏数据。恶心坏了。刚刚超出 size 读一位也不会爆 capacity。就算要用 sanitizer 查出来也比较麻烦,谁会刻意去造一个 2 的整幂的长度来检查代码强度呢?
-
如果不是粗估或者分析,不要做近似!更不要往小了近似!
这个很危险,通常我们认为 \(2^{10}\approx 10^3,2^{20}\approx 10^6\),但是实际上这些估计都很危险——它们都把值估小了。特别是在开空间的时候,尤其不能随便估计!
-
使用
vector
访问指定下标时,一定要注意这个下标是否在vector
的空间范围内。举个例子,做多项式运算,最后查
vector
指定项的时候,如果多项式长度不够就会读到 size 或者 capacity 之外,导致 RE 或者 WA。
实现有误或不精细
-
[HDU6334]Problem C. Problems on a Tree
画蛇添足,本身不需要用
map
的地方偏偏使用了,导致程序及其慢,map
占用了将近 \(\frac 1 3\) 的时间。补充:可以使用
clock()
检查运行时间。 -
[CF446C]DZY Loves Fibonacci Numbers
分块写法,清理标记的时候,没有判断有没有标记:void Normalize( int id ) { //这里缺少了是否存在标记的判断 for( int k = lef[id] ; k <= rig[id] ; k ++ ) //...... }
最开始以为分块的标记和线段树类似,现在才意识到,分块下放标记的时间是 \(O(T)\) 的,所以不判掉就会特别慢......
-
求欧拉路一定要用当前弧优化。
-
对于编码方式比较复杂的问题,一定要注意各种编码是否正确转换了。
比如,二分图匹配的时候,经常会犯忘了特殊处理右部点标号的问题。
-
注意,邻接表建图后,遍历边的顺序是与输入顺序相反的。
-
只要可以离散化,都建议写离散化之后再写线段树。权值线段树实在是太慢了......
当年冰火战士也有这个诡异的坑点。
-
保证复杂度的剪枝写太弱了,导致复杂度是错的。
需要注意写下来的代码(比如剪枝)是否和所想的相同。本质上还是在问思路是否清晰。
在洛谷上居然还可以卡过去,实在是误人子弟。 -
nmd 这个东西是真的诡异。大一点的高维数组寻址奇慢无比,自带满打满算
x2
常数。警告:写比较大的高维数组的时候一定要谨慎,如果可以就开内存池,真的可以有效降低常数。
-
对某个数组进行平移(或者其它下标变换)时,并没能做到对于每个用到数组的位置都去修改下标,最终导致错误访问。
这个问题解决起来比较麻烦,最好的方法还是一开始就确定好如何定下标,避免以后再去修改。
-
少用大常数算法或者大常数数据结构
包括但不限于:
- 可以离散化但不离散化;
- 可以不使用
STL
但非要使用STL
; - 可以使用 BIT 等小常数数据结构但非要用分治等大常数算法;
- 可以使用指针、链表等某些情境下 \(O(1)\) 结构但非要使用更慢的;
- ......
-
注意运算顺序
包括但不限于:
-
动态 DP 一定要先撤销再更新贡献;
-
高精度做比较时,应当是从高位到低维按顺序比较,而不是反过来。
问题是,这个道理虽然简单,但是很容易弄错。
-
-
关于时间戳清空:
如果有多个东西都有可能清空,那么一定不要全局共用时间戳!!!写错了就有可能导致每次修改后所有内容都被清空一次......
如果要用时间戳,则每个可能清空的东西都得开一个标记!。
-
使用“维护前若干最值来达到'可删除'效果时”,需要注意更新最值和删除的元素是否都在同一集合中。
例如,进行点分治的时候,如果求最长路径,则需要保证两点不在同一棵子树。这个时候,维护的最大值和次大值必须是每个子树内先算出一个最值再更新,每个子树仅贡献一个值,而不是每个结点直接去更新。后者会导致剔除元素不完整,有可能出锅。
-
写筛法需要注意的卡常方法:
- 预处理,多多预处理:杜教筛需要预处理 \(O(n^{\frac 2 3})\) 的范围,最后复杂度是 \(O(n^{\frac 2 3})\) 的。但是如果遇到了分块套分块的情况,如果可以线性预处理一部分,则类似于杜教筛地预处理也可以得到 \(O(n^{\frac 2 3})\) 的算法。
- 加速数组访问:其一,尽量进行一维数组访问;其二,函数运算通常慢于宏运算,因此常用的 hash 整除值的方法最好使用宏而不是函数来计算下标(即使这样会略微多进行一次除法运算)。
-
提醒一句,处理负数下标的时候要平移数组。容易在两个地方犯错误:第一是忘记了下标可以是负数;第二是忘了平移或者平移范围不够,还是爆炸。
-
使用扩展欧拉公式降幂的时候,注意可能会出现如下情况:
当我们计算 \(a^{b^c}\bmod p\) 的时候,如果 \(b^c\ge \varphi(p)\) 我们需要保留 \(b^c\mod \varphi(p)+\varphi(p)\)。
然而一般计算过程中我们会让 \(b\) 先去模掉 \(\varphi(p)\)。这样可能出现 \(b\ge \varphi(p)\) 而 \((b\bmod \varphi(p))^c<\varphi(p)\) 的情况,就会算错!
总结一句就是:不仅是算幂的时候取模要小心,对于底数的取模也要特别小心。
数据杂糅
-
题目本身并不难,但是要注意,由于我们将值存储在
Trie
的末尾,所以被删除了的节点本质上就是变成了空节点。因此,空节点存储的值应该等于被删除了的值。例如,如果空节点值为0
,那么被删除的节点的值也应该是0
。否则,
Trie
的结构就可能导致将空节点判断为有值的节点的错误出现(例如,插入一个较长串,并查询它的前缀)。处理方式:同类标记尽量统一。
-
线段树(以及任何静态结构)上,如果需要删除结点,那么在维护最值的时候我们一般会将要删除的结点赋成一个极值。
但是一定要注意,这里的极值应当区分初始化的极值,尤其是在需要同时取出极值所在的位置的时候。这里就需要明确:初始化所赋的极值是可以取的,但是删除所赋的极值就是为了避免取到的。
-
类似的,一些情况下,如果要限制一些值是无效值,我们可能会用 \(\pm \infty\) 来替代,而不能用可取到的极小值!
运算
变量类型
-
整除分块的时候,使用中间变量记录取整除的值,但这个中间变量没有开
long long
:for( long long l = 1, r ; l <= n ; l = r + 1 ) { int val = n / l; // 就是这里!!!应该开 long long!!! }
-
答案的范围是 \([0,2^{31})\),所以计算 \(L,R\) 的时候,如果中途不取模可能会爆
int
。处理方式:涉及到的变量如果非常之大,每一步都取模是必要的;对拍的时候也应该造完全极限的数据。
-
最终求出来的次小生成树的答案可以达到 \(10^{14}\),但是最大值只开到了 \(10^9\)。
处理方法:注意,不同的数据类型应该单独设计最大值,最大值尽量不要多个数据类型通用。
-
最开始,给每个数组开一个
vector
,里面存的是真实值,所以范围是 \([0,10^8]\),用int
完全装得下。后来,发现不应该装真实值,应该装前缀和,所以就改了内容,没改类型,范围变成了 \([0,10^{14}]\),
int
就会爆炸。这样的问题应该在最初编写期就解决,这说明思考还不完全就着急写代码了。
-
像快速幂的指数、求逆元的数这样参数,很容易忽略数据范围是什么。结果就有了
long long
开成int
之类的惨剧发生。 -
斜率优化等可能涉及超大数的运算,如果对于精度要求不高,最好都用上
long double
。想一想:
long double
可能有精度误差,但是long long
一旦爆了,符号直接就错了。像斜率优化这种和 0 比较的运算很多的情景,用long double
显然比long long
要安全得多!
然而这一条需要补充。
double
的准确表示范围和long long
差不多,但是一旦爆了之后,误差就挺大的。尤其是如果低位全部被溢出给去掉了的话,比大小的时候就比较尴尬。结论是:开类型之前先估计一下范围,太大的时候用
long double
,如果long long
开得下那就用double
或long long
。当然,现在用__int128
应该也行。 -
注意存储 hash 值的变量类型。有可能
int
装不下的 hash 值还习惯性地用long long
存了。更搞笑的是,注意更改 hash 值类型。hash 的范围变更之后,很可能出现范围扩大但是值的类型忘了改变的错误。改一两个地方可能还改不对,就有点烦人。
最好是
typedef
或者using
统一一下变量名称。 -
注意运算过程是否会溢出。例如,在目标最大可达 \(2\times 10^9\) 时二分的话,两个边界指针可能在加法时溢出。
某一变量绝对值最大值为 \(L\) 时,执行 \(|x|>\frac{L}{2}\) 的加法需要注意溢出,执行 \(|x|>\sqrt L\) 的乘法也需要注意溢出。
取模安全
其实就是计算过程中,尤其是取模,很容易写着写着就忘记取模了。
简单的加减乘除还比较容易记住,但是进行像自加、自减、赋字面量这样的运算的时候很容易忘记取模。
比较安全的做法是:
- 封装几个函数替代常用的运算,然后在函数内部取模。运算时强制使用它们;
- 封装模域类,然后只用这个类运算。这个对于多模数的情况比较友好;
常见的问题有:
-
注意特殊的模数,尤其是题目输入模数的时候,注意模数是否可能为 1。
例如,有的题目取模的模数是单独输入的,特别注意模数可否为 1,特别注意赋的初始值是否落在正确的范围内,不确定可以取一下模;
没有把握就在输出取模,
虽然这看起来也只是权益之计。 -
注意模数到底是不是质数。如果模数是输入的,则需要注意。同时,如果模数确定但并非常见的质数,也一定要验证一下它到底是不是质数。
毕竟,是质数与否差别很大!
-
又是不定模数的问题。
当模数比较小的时候,计算组合数 \(\binom{n}{m}\) 是比较危险的,尤其是当 \(n,m\) 有可能大于等于模数的时候。
此时,组合数必须使用 Lucas 定理来计算,甚至还需要用上扩展 Lucas,这是后话。
浮点数
-
「Gym102798E」So Many Possibilities...
实数概率 DP,因为 \(\epsilon\) 设得太大,导致用它判空状态的时候将有效状态也判成空,漏了不少结果,答案偏小......
用实数算 DP 的时候,没有必要用上 \(\epsilon\)。需要用来卡常的时候,算好可能的系数量级,然后反推 \(\epsilon\) 的大小;宁可设得小一点,也不要漏掉有效状态。不要再出现这样不知所谓还被卡了半天的错误!
比较特别的情况。这道题里面会出现 \(2^{-m}\) 这样极小的系数,如果再加上 \(\epsilon\) 的因素,有的行可能根本就不会参与消元!
总结起来就是:\(\epsilon\) 比较不是必要的,尤其是对精度要求很高的时候!
-
模拟赛题目。
需要对浮点数进行 \(10^6\) 组运算,每组运算
+,-,*,/
都齐了。结果就
不出意外地爆掉了double
的精度,只有开long double
才能通过。注意
double
在大数小数混合运算时的精度问题!!! -
注意任何数学运算是否安全。
例如 「POI2011 R1」避雷针 Lightning Conductor 这一题,需要注意的细节是,实值函数 \(a_j+\sqrt{|i-j|}\) 才具有单调性。虽然实际求答案的时候,\(a_j+\sqrt{|i-j|}\) 和 \(a_j+\lceil\sqrt{|i-j|}\rceil\) 是一样的,但是前者是实值函数,后者是整值函数。而在整值函数上单调性已经被破坏了,因此只要涉及到与决策有关的比较,都必须用实值而非整值。
-
浮点数遇到除法就应当注意,是否会除以 0。
有的时候可能根本想不到......
比如说「JSOI2004」平衡点,对于某个向量计算 \(\vec e\) 的时候,没有注意的话就可能遇到 \(\vec 0\),就会 RE 或者算出
nan
。 -
对于大数慎用
double
和double
系函数。譬如在 \(|x|\le 10^{18}\) 的范围下,使用
floor(x)
会有明显的精度误差,所以尽量不要用floor()
。double
真正存数字的 bit 甚至少于long long
,所以单独存整数的话精度比long long
还差。实际上计算 \(\lfloor\frac{n}{m}\rfloor\) 可以根据 C++
n/m
的计算方式进行修正,从而规避floor()
。
STL 的奇妙特性
-
众所周知,为了
使操作更麻烦简化操作,STL 的set
和map
等关联容器内部基本上都提供了reverse_iterator
这种迭代器。从字面意思就可以知道,这种迭代器是逆向访问容器的。比如
set
默认使用 < 比较,如果用reverse_iterator
来访问它则会从大到小遍历set
的元素。相应地,容器也会有rbegin()
和rend()
这样的函数,一看就懂了。问题是,
reverse_iterator
的运算也是和iterator
呈镜像对称。比如++ reverse_iterator
,那么从set
原本的顺序来看,就相当于向变小的方向移动,反过来也类似。如果和
iterator
混用就很容易弄错,比如今天上午我就调了 0.5 h。此外,一般来说容器的
erase()
都只会接受iterator
。既不可以往里面丢reverse_iterator
,也并不存在rerase()
这样的函数。 -
对于
vector
这样使用random access iterator
的容器,一般来说使用迭代器访问的效率低于下标访问。另外,如果用
for( x : y )
这样的循环,那么内部是使用迭代器实现的,因此需要注意大数据下的运行效率。 -
set
如果删除不存在的元素,那么它不会RE
,而是会直接略过这一次操作。相当于是它会自己检查元素是否存在。 -
vector
的resize()
只会对新添加的元素执行构造函数(或者进行指定的初始化,如果存在第二个参数的话),不会修改已有的元素。 -
STL
真的真的真的很慢!!!,如果能自己手写一些替代的数据结构,就少写一点 STL!!! -
关于新库
<random>
中的函数:像
uniform_int_distribution
这样的分布类,它需要接受一个类型,并且存在默认参数为int
(应该是int
)。但是,如果在生成
int
类型的变量时经常偷懒,则在生成其它类型是很容易忘掉填写类型。这样的后果是,在某些 OJ 上编译执行会得到 MLE 的结果实质上写了一个 UB。最离谱的是,这样的错误在 Windows 和 NOI Linux 2.0 下面都不会报错(因为编译器难以检查),所以只能防而很难治。
不过在 NOI Linux 2.0 下编译并用 gdb 调试似乎还是可以发现这个问题的。但是不用虚拟机就比较麻烦了。
-
vector
的“动态”实现方式:只考虑元素增多的情况。当
size
超过capacity
的时候,vector
会新开一段长度两倍于当前capacity
的内存,然后给旧的元素全部搬家到新的内存上,然后再加入新元素。这就意味着,如果遇到了
vector
扩容的情况,则旧的迭代器、旧的指针会全部失效。因此,不建议长期保存变化的vector
的指针或迭代器,保存下标倒是可以的。
读题有误
-
有点奇怪。题目里面说
There is no pipe which connects nodes number 1 and N
,但是它的真正含义是不存在从 1 流向 \(n\) 的管道,但是可以存在从 \(n\) 流向 1 的管道。这个时候就会出现环流,因此需要建立新的源点与汇点,避免“源”和“汇”出现环。思考的时候要细致一点,每个方面都要想到;同时对于题目理解要清晰到位,结合好题目前提与背景。
-
丢人啊直接看到一个 \(0\le k_i\le n\),就以为是“所有的问题编号都落在 \([1,n]\)” 里面了。
草我究竟是怎么联想到它的然后直接不搞离散化,成为挂掉 100pts 的 nt 选手。
其它
-
奇妙的题目。构造割的时候,如果源点的出边全部满流了,那么一种割就是 \(\newcommand\set[1]{\{#1\}}[s,V\setminus \set{s}]\),然而这显然不是我们想要的。
为了避免这种情况,我们必须调整答案,使得源点的出边略有剩余容量;具体操作起来就是让答案变小一点,而后从 \(s\) 开始遍历寻找一组割。
-
任何时候,使用 DFS 传回
bool
信息表示某个过程是否已经结束时,应该在任何一次递归调用结束之后,查询返回值并判断是否应该结束过程。否则会导致各种乱七八糟的问题。 -
多组数据,注意输出换行!!!
不是开玩笑,尤其是在用
cout
输出的时候。由于不常用,就很容易忘记输出endl
。 -
最大生成树,运算符直接重载为了小于。
-
循环变量用上了不常用的名字,结果之后就写错名字:
for( 对 i 循环 ) for( int u = 1 ; i <= n ; u ++ ) //......
处理方式:尽量规避奇怪的循环变量名称。