精度
计算 x 的位数的代码是 int bit = log(x + 0.5) / log(10) + 1;
或者 int bit = ceil(log(x) / log(10)) + 1
。即先算出 double 类型的答案,向上取整后 +1 ,再存到 int 类型里。
如果错写成 log(x) / log(10) + 1
,对于 x = 1000,(在我测试的电脑上)会计算出 3,而正确结果是 4 。
紫书里面提到了,对某个数开根要写成 int tmp = sqrt(x + 0.5)
,否则有精度差。
就是这两个地方,注意写的时候 +0.5 来避免类似 x.99999828 的数被截尾算作 x (即避免精度差)。
vector 存边
最常见的边遍历
vector<edge>::iterator it;
for (it = G[x].begin(); it != G[x].end(); ++it) {
//...
}
有时候会写成
for (i = G[x].size() - 1; i >= 0; --i) {
//...
}
注意这里 i 必须是 int ,如果 i 是 long long 就必须写成 i = (long long)G[x].size() - 1,否则会出些奇怪的错误。所以最好直接给 i 开 int 。
可以测试一下:
int tmp = 5;
printf("%lld", tmp - 1);
结果不是 4 。
printf("%lld", 5 - 1);
结果也不是 4 。
upd: 用笔记本试了下,能跑出 4 ,但是机房电脑不会。反正避免就是了。
fread
甚至强过头文件优化(雾)。使用方法同 getchar()
函数。
char buf[1 << 20], *p1, *p2;
inline char void gc()
{
return p1 == p2 && (p2 = (p1 = buf) + fread(buf,1,1<<20,stdin), p1 == p2) ? EOF : *p1++;
}
next 数组
$ k = 0, 1, 2, 3 $ 分别表示上下左右,然后希望找一个对应关系 f(上) = 下, f(左) = 右,反过来也是。这样的话这个对应关系 (f(k) = k xor 1) 。
求区间严格第 (k) 大值
正解是树套树,裸题有“二逼平衡树”。但是有的问题 (k) 比较小,直接线段树维护就可以了。假设左区间前 (k) 大值分别是 (a_1,a_2,a_3...) ,右边是 (b_1,b_2,b_3...) 。合并后当前区间的前 (k) 大值是 (x_1,x_2,x_3...) 。
考虑求 (x_i) ,枚举 (a_1,...,a_i) 和 (b_1,...,b_i) 。如果 (a_j < x_{i-1} left(1 leq j leq i
ight)) ,就用这个 (a_j) 去更新 (x_i) 。对 (b_j) 同理。一次合并的时间复杂度是 (O(k^2)) , (k) 小的话就可以视为常数,所以基本上不会破坏线段树的时间复杂度。
树上路径求交
求出两条路径的 (lca) 记为 (g, h) ,如果
- 深度相同, (g e h) 则无交集, (g = h) 则有交集。
- 深度不同,用深度较大的 (lca) 作为 (t) ,假设另外一条路径 ((x, y)) ,则 (t) 应在 ((x, y)) 上才有交集,即:
当两条树上路径有交集时,画图并且分 (5) 类情况(考虑根的位置)可知,((a, b)) 和 ((c, d)) 这两对树上路径的交的两端是 (lca(a,c), lca(a,d), lca(b,c), lca(b,d)) 这四个点里,深度最大的两个点。
树上 LCA 关联的点
有时候边权化点权会用到,肯定可以算出深度之后重新向上跳,不过树剖的时候也可以这么搞:
while (Top[a] != Top[b]) {
if (Depth[Top[a]] < Depth[Top[b]]) {
b = Dad[xb = Top[b]];
} else {
a = Dad[xa = Top[a]];
}
}
if (Depth[xa] == Depth[lca] + 1)
insert(xa);
if (Depth[xb] == Depth[lca] + 1)
insert(xb);
if (a != b)
insert(Heavy_son[lca]);
树上换根对子树影响
假设原树的根是 (x) ,新根是 (y) 。讨论 (t) 在新树中的子树。
- 如果 (y) 就是 (t), (t) 在新树中的子树就是整棵树。
- 如果 (y) 在原树中 (t) 的子树外,那么新树中 (t) 的子树仍然是原树中它的子树。
- 如果 (y) 在原树中 (t) 的子树内,设 (t) 在 (y) 到 (x) 的路径上的儿子是 (g) ,而原树中 (g) 的子树是 (s) ,那么新树中 (t) 的子树是整棵树除去 (s) 这部分。
二分查找
STL 里有的容器(比如 set 和 map)自带 lower_bound()
,这是因为这些容器有一些非常优秀的结构,用 a.lower_bound(val)
可以利用到这些结构。因此这样做的速度远快于单纯的二分查找 lower_bound(a.begin(), a.end(), val)
。
图论题小细节
- 无向边用链式前向星存的时候记得开两倍边的空间。
- Tarjan 缩点之后用 Topsort 搞入度之类的东西的时候,记得考虑自环和重边——自环往往对入度并不应该有贡献。
- 看清无向图还是有向图
(因为这个爆零两次了)。
子集求和
从 nodgd 的一场训练赛里学到的新技能。就是 (O(n cdot 2^n)) 算 (f(S) = sum f(T) (T subsetneq S)) 。
实际上就是一个多维前缀和。比如说,二维前缀和可以看做先算每行的前缀和,然后再算新的矩阵中每列的前缀和。同理,多维前缀和依次算每一维(代码中的 i )的前缀和。
听说有个叫快速莫比乌斯变换的东西 Orz 。
for (int i = 0; i < M; ++i)
for (int j = 0; j ^ FUL; ++j)
if (j >> i & 1)
f[j] += f[j ^ 1 << i];
换行符
爆零警告: Linux 下换行符是 '
'
。不要用“是换行符”作为输入结束的标识,而应该用“不是合法字符”
C++11 新特性(注意 NOIp 不能用,省选不一定能用)
mt19937
据说可以得到 unsigned int
范围内的随机数。注意 time(NULL)
在对拍时由于程序跑得太快,可能会使两个程序有相同的 seed 。 chrono
库提供了更精确的计时函数,所以基本上不会出现上面说的情况。
#include <cstdio>
#include <chrono>
#include <random>
using namespace std;
mt19937 mand(chrono::steady_clock::now().time_since_epoch().count());
int main(){
printf("%u
", mand());
return 0;
}
tuple
制作 tuple
tuple<int, int, int, int> tp = make_tuple(a, b, c, d);
解开 tuple:
int a, b, c;
tie(a, b, c, ignore) = tp;
或者
int a, b, c;
a = tp.get<0>();//等价于 a = get<0>(tp);
b = tp.get<1>();
c = tp.get<2>();
编译命令
- 本地有庞大的递归调用需要加
-Wl,--stack=一个很大的数
,不然并查集都可能会爆栈。 - 开启 C++11 需要加
-std=c++11
。 - Lemon 在 32 位下评测时需要加
-m32
以生成 32 位代码。
链式前向星的清空
只需要清空 first 数组就可以了,有的时候这会大大影响程序效率。
主席树
记住每次新建,写
if (!p)
new_node(p);
的后果是调试到死。