OI 中一些可能有用的小 Trick 与注意点
停止更新。
图论:
杂项:
- 树上边权转点权转移到儿子节点,但是特别注意多余信息处理(尤其是树剖的时候)
比如树剖结束的时候处理最后一条重链,最顶端节点的答案不能处理进去(因为会有多余信息 \((top_x,fa_{top_x})\))
还有一些计数题比如说统计路径上有多少个不同颜色的,注意一下根节点的多余数据处理(因为根节点是没有颜色的)。 - 看到图论题先想一下这几个问题:图是否连通?有向图还是无向图?是否带权?有没有重边与自环?
- 遇到矩阵问题不要老是想和矩阵有关的算法,有些时候或许只是道简单的图论。可能每个点是一个点,也可能一列一行是一个点,也有可能是每个网格是一个点,然后边权是被割断的边的边权(交通规划),都有可能。
- 看清楚是有向图还是无向图,无向图空间要开 2 倍!网络流的边数与点数不能算错!
- 缩点的时候注意不能将原图和新图搞错。
- 如果需要使用各种 连通块内的算法(比如最短路,网络流等等),无论图是否连通,最好建立 虚拟起点/虚拟终点(网络流有个词叫 超源超汇),强制使这张图变为连通图。
- 树的直径有一个很好的性质:
如果一棵树的直径长度为偶数,那么这棵树的所有直径一定交于一点,并且这个点是直径的中点;
如果一棵树的直径长度为奇数,那么这棵树的所有直径一定交于一边,并且这条边的中点是直径的中点。 - 写 dfs 序的时候可以用两个数组,\(l_i\) 是正常的 dfn,\(r_i\) 在遍历完子树后记录一下当前 cntdfn 的值,这样 \([l_i,r_i]\) 就是子树内所有点的 dfn。
网络流:
- 网络流中拆点是很常见的一种技巧。例题。
- 网络流中计算点数的时候注意不要忘记 拆出来的点与超源超汇,在采用当前弧优化的时候这些点也是需要算进去的。
- 网络流中出现阶段性问题的时候(比如时间),一般就是分层图上的网络流问题,此时算好 点数与边数的极限上界,并且不要采用 ISAP 求解最大流(直接二分答案除外),因为 dinic 可以直接在新图的残量网络上跑。例题。
数据结构:
- 数据结构题不要只想 \(\log\) 结构,分块它不香吗?根号分治它不香吗? 莫队它不香吗?
- 有些 DS 题会问你一个区间 \([l,r]\) 内需要划分成至少几个子序列以满足题意,比如这道题,这个时候有一种思路就是先双指针处理出每个点往右边能够预处理的最右点,然后倍增即可。
- FHQ Treap 在处理区间信息的时候如果需要维护懒标记的时候需要注意
Merge()
函数中x,y
都需要下传懒标记。典型例题。 - 无论是什么 ds 题,一定要将想法往可合并性上靠(莫队/分块:?)。
- 如果出现区间取模/取对数/开根号之类的玩意,可以考虑这玩意取模会砍半/取对数与开根号数字降的很快等从而可以暴力做并且复杂度正确,并且该情况下单点修改操作也允许并且复杂度也是对的。
DP:
- 遇到类似于 \(n\) 个东西分成两组,差最小等问题,不要总是想着一些奇奇怪怪的算法,如果就是个容量为 \(\dfrac{n}{2}\) 的背包问题呢? 此时的体积和价值都是这 \(n\) 个东西的属性。
- 如果发现题目是个 \(O(n^2)\) 的 DP,别多想,状态只能是 \(f_{i,j,k}(1 \leq i,j \leq n)\)(第三维没有或维数为常数) 或者是 \(f_i\)(1D/1D 的 DP,不过这个一般能优化到 \(O(n)\)/\(O(n \log n)\) 之类的)。
数学/数论:
- 对于一张无向图的邻接矩阵而言,如果 \(A_{i,j}\) 表示 \(i\) 与 \(j\) 之间是否有连边,那么做矩阵快速幂 \(A^k\) 之后 \(A_{i,j}\) 的值表示从 \(i\) 出发走 \(k\) 步有多少方案到 \(j\)。
- 位运算三大处理方法:线性基(异或专属),拆位,01Trie。推式子时可能 \(a\oplus b=a\mid b-a\& b\) 会有用。
- 拆位处理二进制时,注意或得到 0 和与得到 1 的限制是更严的,做题先考虑这一点。
- \(\gcd(a,b)=\gcd(a,a+b)=\gcd(a,a-b)\)。
- 枚举 \(S\) 及其子集,\(O(3^n)\)(其中 S 占了 \(O(2^n)\)):
for (int r = s; r; r = (r - 1) & s)
,复杂度证明考虑枚举子集 \(O(2^{|S|})\),然后写出式子二项式定理化简。
小技巧:
- 遇到区间问题多想想差分,尤其是树上差分。
- 类根号分治思想真的是非常常见又好用的一种思想!
- 看到题目中有回溯操作时,除了可持久化外也可以想想建立操作树。
- 当你想不出题目的时候不妨考虑一下随机化乱搞比如说模拟退火或者是随机权值和 hash/xor hash。
杂项
- 关于优先队列的重载运算符问题:
注意优先队列是一个大根堆,因此重载小于号的时候如果重载函数里面写的是 \(fir.a < sec.a\),这表示 \(fir\) 比 \(sec\) 小,因此 \(sec\) 要排到前面。
就像这样:
假设要比较的是 \(a<b\),要使优先队列里面 \(y\) 为第一关键字升序排序,就需要根据大根堆的性质重载小于号为大于号struct cmp { int x, y; bool operator <(const cmp &fir) const { return y > fir.y; } };
话说怎么这么绕,这样才能够保证 \(a.y<b.y\) 的时候 \(a\) 在 \(b\) 前面。 - Meet in meddle,二分 / 三分,启发式合并,倍增等算法别忘了。
- 判断数 \(s\) 第 \(t\) 位是否为一写法是
((s >> t) & 1) == 1
或者(s & (1 << t)) > 0
(注意不是等于 1)。 - 尽量全开
long long
,不过#define int long long
不推荐。