「WTF」铁锅乱炖-算法篇
数据结构
分块
-
[Ynoi2019模拟赛] Yuno loves sqrt technology II
写莫队发现自己没有排序,T 飞了。大致可以描述为:
void Init() { //对 Q 数组进行排序 } int main(){ Init(); ...... //读入 Q return 0; }
处理方式:一定要将数据读入完之后再预处理 。
-
题目见上。
写分块的时候对边角块处理不当......
void UpdateBlock( int id, int l, int r, ... ); //块内处理 void UpdateAll( int L, int R ); //全局处理 { if( 左边有边角块 ) UpdateBlock( 块编号, L, R, ... ); ...... }
事实上应该将左右边界限制在块的范围内。
-
其一:分块修改时,应该枚举完整个 \([lef, rig]\) 的区间,而不是只枚举 \([qL, qR]\) 内的数。这种情况主要出现在区间的元素被重排了的情况。
其二:无论是分块还是线段树,一定要注意将交集为空的情况判掉!
-
带修莫队的普遍问题:修改需要对于整个状态区间都造成影响,但是仅当修改落在当前询问范围内时,我们才需要更新答案。
堆
-
使用了俩优先队列的 " 可删堆 " ,结果在
push
和erase
的时候都没有清除多余元素,于是堆中冗余元素就超多,结果 TLE 了:typedef priority_queue<int, vector<int>, greater<int> > Heap; struct RHeap { Heap q, rub; void Push( const int v ) { q.push( v )/*这里需要 Maintain 一下*/; } void Erase( const int v ) { rub.push( v )/*这里需要 Maintain 一下*/; } int Size() { Maintain(); return q.size() - rub.size(); } int Top() { Maintain(); return q.empty() ? 0 : q.top(); } RHeap() { while( ! q.empty() ) q.pop(); while( ! rub.empty() ) rub.pop(); } void Maintain() { while( ! q.empty() && ! rub.empty() && q.top() == rub.top() ) q.pop(), rub.pop(); } }
上面的“问题”已经破案了,真正的原因是
数组开小了。但是谨慎起见,最好还是每次加入
maintain
一下。👍额外提醒一点,惰性删除堆千万不要删除不存在的元素。
线性结构
-
这道题用单调栈维护所有位置的最值的和的时候,细节很多。尤其注意的是,每个点管辖的区间实际上是从栈内的上一个元素开始的,也就是 \((stk_{i-1},i]\) ,而不能将自己作为自己那一段的终点。请参考以下这段代码:
while( top1 && a[stk1[top1]] > a[i] ) mn -= 1ll * ( a[stk1[top1]] - a[stk1[top1 - 1]] ) * ( i - stk1[top1 - 1] - 1 ), top1 --; // 这里不能写 i-stk[top1] ,不然就会少算一些位置 mn += 1ll * ( a[i] - a[stk1[top1]] ) * ( i - stk1[top1] - 1 ) + a[i], stk1[++ top1] = i;
线段树
-
写了两棵线段树,大小不一致,并且查后继时如果没有后继则默认为 \(siz+1\)(但实际上应该是 \(+\infty\))。这就导致了在较小的树上查后继的时候,得到了较小的范围,而在较大的树上查询时会查漏一些点。
处理方法:注意大小关系,注意某些特殊值的取值(无穷大无穷小等),尤其是大小不一致时容易翻车。
-
笑麻了,这都 1202 年了,居然还可以写错线段树。
具体来说,线段树区间修改,因为递归边界有锅,导致运行起来与暴力无异:
void Update( const int x, const int l, const int r, const int segL, const int segR, const int delt ) { if( segL > segR ) return ; if( l == r /*segL <= l && r <= segR*/ ) { Add( x, delt ); return ; } // 这里写错了 int mid = ( l + r ) >> 1; Normalize( x ); if( segL <= mid ) Update( x << 1, l, mid, segL, segR, delt ); if( mid < segR ) Update( x << 1 | 1, mid + 1, r, segL, segR, delt ); Upt( x ); }
平衡树
-
第一次犯错:以为区间反转对应的端点可以直接算出来,结果需要用
kth()
来找;注意相对顺序会改变。第二次犯错:
kth()
的时候没有下传标记。序列平衡树很久没碰过了 qwq ......
-
怎么又是平衡树:写非旋 Treap(或者其它任何结构)在结构变化的时候,一定要在结构确定下来后,更新维护的信息。
例如,非旋 Treap 就需要在子树分裂完成和子树合并完成后及时更新信息。
-
平衡树上二分,有时候可能会跳父亲(比如用全局平衡二叉树加速链上移动)
这个时候,需要注意右儿子的父亲必须直接跳过,因为这些结点并不对应序列上的移动,它们仅仅是建立了树的结构......
-
从序列上建立平衡树的时候(尤其是分治构建),尤其需要注意叶子结点的初始化。有可能到了叶子结点直接返回了!
-
不知道这个有没有专门的名称,应该叫“珂朵莉树”吗?
反正就是用
set
维护连续段的技巧。由于连续段都是极长的,因此进行单点加入删除的时候可能存在连接两段的情况。这一点可能被忽略,因为其它的判断都不会同时涉及相邻两段。注意维护一下这种情况。
-
替罪羊树,重构的时候记得重新设置新根的父亲编号。
另外,替罪羊树实现后缀平衡树的时候,权值范围需要开大一点,不然算着算着误差和真实差值同量级就不妙了(真实差值大概也就是 \(2^{-\log n}=n^{-1}\) 嘛)。
并查集
-
带权并查集相关:
例如 「LG P5787」二分图这道题,很多时候,带权并查集维护的是一棵生成树上,某个结点到根的信息。那么在连接 \((u,v)\) 这条边的时候,从 \(u\) 所在的根 \(r_u\),到 \(v\) 所在的根 \(r_v\),实际上是先到 \(u\),再经过 \((u,v)\) 到 \(v\),最后到 \(r_v\),因此合并信息的时候,也应该按照这条路径来合并。
-
可撤销并查集相关:
其一,断开从 \(u\) 到 \(v\) 的父子边的时候,\(u\) 的父亲应该还原成 \(u\) 而不是 0。
其二,可撤销并查集不能写路径压缩,不能写路径压缩,不能写路径压缩!!!
标记问题
-
标记处理的问题:
-
注意:左偏树删除堆顶的时候,它的标记可能还没有下传。因此需要下传标记之后再合并左右子树。
有的数据结构也是同样的。如果在删除的时候,被删除的节点对于其他节点的影响需要全部清除。
-
-
自底向上的结构,每次更新之前必须把所有标记全部下放。由于懒标记只能做到在父亲处查询结果正确(有点蒙混过关的意思),所以即便是自底向上更新也必须下放标记,否则信息会被“刷新”而非“累积更新”。
图论
生成树
-
使用 Boruvka 算法的时候,一定要注意,找出的是点,对应的是连通块,并查集维护的根与连通块的编号没有任何关系,千万不要用混了!
-
建立最小 xor 生成树的时候,某个结点的两个儿子的连通块之间只会连上一条边,建 Kruskal 重构树的时候别搞错了。
网络流
-
网络流的建图:
调了一个半小时,终于发现问题所在:竟然是反向边弄错了,最初
cnt
赋值为 0 而非 1 !!!处理方法:单个变量不要忘记初始化!
-
一定要注意,如果用 Dijkstra 跑费用流,那么每次最短路求完之后一定要修改势为当前最短路的结果。
设在当前完成了最短路之后,结点 \(u\) 的最短路标号为 \(d_u\),那么在一次增广后,原先图上的边 \((u,v,w)\) 仍然满足 \(d_v\le d_u+w\)。而如果当前边有流经过,反向边 \((v,u,-w)\) 被加入图中,那么 \((u,v,w)\) 必然在原图最短路上,也即 \(d_v=d_u+w\),反过来也可以得到 \(d_u=d_v-w\),所以此时 \(w'=w+d_v-d_u=0\),仍然是非负的。
最初的时候我们会跑一次最短路求出势,而这个势只是对于原图有效,增广后就不能再使用它了。由于原图上可能有一些特殊性质,因而我们可以用非 Bellman-Ford 类算法求出它,以获得较好的复杂度。
-
网络流图上尽量不要建无用边。比如,这道题如果没有控制边是单向的(没有保证从一部流向另一部),则会导致常数大很多,跑起来很吃力,尤其是边比较多、图比较大的时候。
最短路
-
关于 SPFA 判断有无负环的注意事项:
-
SPFA 只能判断从起点出发能不能遇到负环。如果有一个负环不能从起点出发达到,这个负环就碰不到。
-
一些简单的优化有极佳的效果,例如
dist[]
初值设置为 0 和 DFS 版 SPFA 在负环判断中表现良好。
-
-
在进行优化过的图上,如果存在一些不可作为终点的结点,需要注意处理它们的初始值。这一点在 DAG 上用拓扑序跑最短/最长路的时候尤其需要注意。
比如 「NOI2015」小园丁和老司机,就需要注意给某些新增的结点(比如前后缀结点)赋特殊的初值,再跑 DP。
二分图匹配
-
二分图匹配的匈牙利算法:
枚举点搜索增广路的时候,理论上如果点已经匹配,那么再去搜索就是无效的,甚至可能导致错误。
......然而我没有判掉这种情况,甚至愉快地通过了 uoj 的模板题.....
-
二分图最大权匹配的 KM 算法:
话说这个东西貌似也可以叫做匈牙利算法。如果某些点的
slack
还不够小,那么记得修改slack
而不是让它保持原样。rep( i, 1, N ) if( ! visY[i] ) { if( slk[i] > delt ) slk[i] -= delt; // REMEMBER to reduce slack due to change of labX !!!!! else { ... } }
实现时的其它注意事项:
-
KM 的数组很多,清空的时候名字不要搞错了
-
KM 实现的时候,
pre
指针对于未被遍历过的点来说是可动的,但是一旦某个点被遍历过了,对应的pre
就不能再改变了。此时的pre
表示的就是被遍历到时这个结点的父亲。
-
树上操作
-
分治过程中,如果在点上有信息或限制,并且运算不满足幂等性,那么应当保证分治重心的信息不会被重复统计,在“统计”和“存入”的时候需要注意是否应当计入分治重心。
-
一道重链剖分的题目,写错了两个地方:
-
其一,居然把重链剖分写成了轻链剖分😢,就一个符号写错了;
-
其二,中途某个位置没有开
long long
,一直没有检查到;
处理方法:第一个问题属于写错了只会影响复杂度的类型,应该多测一下极限数据,什么样子的都应该测一下;第二个问题则应该检查每一个变量类型开得是否合理。
-
-
关于不同颜色计数的树上差分方式。
如果按照 DFN 排序之后,某个颜色之前和之后都有颜色出现了,那么应该选择一个较低的 LCA 来删除多于贡献,而不应该在两个 LCA 上都删除一遍。
-
树上动态 DP:
首先,退贡献的时候应该用旧的信息去退。比如说保险的方法是,从祖先到儿子退贡献。
其次,为了避免出错,修改的时候最好每次只修改一个点的信息,也就是说,最好不要同时退两个或以上结点的贡献。
其它
-
判断(半)欧拉图、求欧拉(回)路一定要检查图是否连通!!!
-
在图上进行记忆化搜索的时候,如果没有搜索到需要的结果,那么也要声明某个状态已经被搜索过了,不能把状态留在那里等着其它起点再去找一遍,浪费时间。
数学
-
注意,Pollard Rho 的原理是,假如 \(n=p\times q,p<q\),那么我们随机生成一组数 \(x_1,x_2,\dots,x_k\) 之后,如果 存在 \(i,j,x_i\equiv x_j\pmod p,x_i\not\equiv x_j\pmod n\),那么 \(\gcd(x_i-x_j,n)\) 一定会生成一个 \(n\) 的一个非平凡因子。
因此,朴素做法是枚举环长,也即 \(j-i\),并取 gcd;而加上 Floyd 判环法之后则是枚举 \(i\) 并检查 \(2i,i\) 两个元素。加上了倍增,其实也就是在倍增环长,因此需要每次和路径首个元素做差,而非和前一个元素做差。
又把 Pollard Rho 写错一遍.jpg。
错误一:对于迭代方程 \(x_{i+1}=x_i^2+c\),其中初始值 \(x_0\) 和 \(c\) 都应该在范围 \([1,n-1]\) 中生成,否则在 \(n\) 非常小的时候会翻大车。
比如,当 \(n=4\) 的时候,很容易直接陷入死循环。
错误二:如果样本累积若干次后,变成了 0,就应该直接退出该次取样,而不能取 \(\gcd\) 之后以为找到了非平凡因子。
-
注意,旋转的时候是寻找 \((i,i+1)\) 这条边的对踵点,因此假如为 \(j\),那么应该检查 \(\operatorname{dist}(i,j)\) 和 \(\operatorname{dist}(i,j+1)\)。
当然将四个点对全部检查一遍自然没什么问题 -
关于网格图上高斯消元的问题。
设网格大小为 \(n\times m\),且 \(n,m\) 同阶。这类问题一般有两种解决方案:
-
带状矩阵高斯消元,本质上就是利用位置 hash 的性质偷了个懒,大大减少了所需扫描的范围。复杂度为 \(O(nm\times \min\{n,m\}^2)\)。
但是这个方法在网格图消元之外比较危险。网格图上可以保证对角线上系数为 1,但是其它问题中就不一定了。如果出现了需要换行的情况就比较麻烦,不好好处理很容易直接破坏掉带性质;一般来说,如果需要换,那么我们会选择换列而非换行,这是因为如果某一列之下有超出带的范围的系数,我们可以在之后的消元中解决掉它。
-
确定主元,这个相对来说适用性更广,效率更高,可以做到 \(O(n^3)\),但是写起来较为复杂......
-
-
关于 BSGS 算法和扩展 BSGS 算法:
一句话:求离散对数的时候一定要注意 \(p=1\) 和 \(b=1\) 的特殊情况!!!,尤其是模数是直接读入且可以 \(=1\) 的时候!
此外,这里出现了 \(0\) 的幂的问题。注意,这里的 \(0\) 实际上必然是对 \(p\) 取模过后的结果,它原本对应的是一个正整数,所以这里实际上是 \(0^0\equiv 0\pmod p\),其它题目中也一定要注意这样的细节!
注意 BSGS 折半的上界是 \(\lceil\sqrt p\rceil\),这个 \(\lceil\rceil\) 不能漏了!
-
数学真奇妙;没有交换律真奇妙。
\(n\) 阶方阵群是一个典型的半群,这上面没有交换律。
相应地,系数为 \(n\) 阶方阵的多项式群也是半群,这上面也没有交换律。
在这上面进行多项式求逆的时候,传统步骤中的一步:
\[(G_0-G)^2\equiv O\pmod {x^n} \]展开之后得到的就是:
\[G_0^2-GG_0-G_0G+G^2\equiv O\pmod {x^n} \]没错!半群上没有交换律,完全平方公式失效了!!!
由于 \(GG_0\) 和 \(G_0G\) 同时存在,这个方法已经走不下去了。可行的方法是从 \(G_0F-I\equiv O\pmod {x^{\lceil\frac n 2\rceil}}\) 出发:
\[\begin{aligned} (G_0F-I)^2&\equiv O\\ G_0FG_0F-2G_0F+I^2&\equiv O\\ G_0FG_0-2G_0+G&\equiv O\pmod {x^n} \end{aligned} \]最后移项即可得到 \(G\)。又有一点需要注意,半群上 \(a^2b^2\not=(ab)^2\),所以中途出现了 \(G_0FG_0F\) 这种东西。
-
关于高斯消元。
之一:高斯-约旦消元的精度似乎比高斯消元的精度要高。但是据说如果高斯消元不处理精度误差那么就不会出现问题。
之二:跟行列式不同,需要将单行的主元系数化一。
之三:进行高斯-约旦消元的时候,一次消元过程中每一行内所有的元素都必须参与消元,不能像高斯消元那样只处理一个后缀上的值!
-
多校赛某题。
\(n=10^{10}\) 的时候做杜教筛,预处理范围只有 \(10^6\),导致杜教筛跑得非常慢......
处理方法:熟悉时间复杂度原理,杜教筛需要预筛到 \(n^{\frac{2}{3}}\) 才可以。
-
同余问题:注意,如果算出来的解形如 \(x\equiv x_0\pmod M\),则一定要注意,解有无穷多组。
比如,如果输出前 \(k\) 小的解,则就算 \([0,M)\) 中解的数量不足 \(k\) 个,也不会出问题——因为在模意义下,有一个解就有无穷多个解。
此外,还需要注意,如果是求“正整数”解,则尽管我们得忽略 0,我们不能忘记 \(kM,k>0\) 这样的解是有可能可行的。
-
输入一个分数,并且需要对这个分数本身做研究时,注意它是不是最简的分数,如果不是应当化成最简的形式。
比如,将分数转化为小数时,分数是否最简就相当重要!
-
Karatsuba 分治乘法不能对于乘起来的结果截断。因为上一层分治需要下一层算出来的完整信息才可以算出来这一层的信息,截断之后信息就缺失了!
简单的调试方法是:Karatsuba 有一个阈值,用来判断做暴力乘法还是分治乘法。把阈值调小一点测试就行。
字符串
-
后缀数组模板。
注意中间的“桶”数组大小与字符串长度相同,而非与字符集大小相同!
const int MAXN = 1e6 + 5, MAXC = 300; int buc[MAXN]; // 注意不是 MAXC
-
关于回文自动机 「SHOI2011」双倍回文。
回文自动机的 fail 树上经过的回文串,串长的奇偶性和出发的根没有关系。比如,从 0 根出发,并不能代表 fail 树上的串都是偶串,它只能保证自动机上的串都是偶串。
-
关于 Trie 树和其它数据结构的空结点。
空结点必须要和根节点区分开来。我们需要保证空结点必须要转移回空结点,但是如果偷懒将根设置为 \(0\),则此时如果失配就会回到根节点而不是去到空结点!
思考的漏洞
-
发呆想了半天才发现枚举的那一天可以放在钦定的前 \(k\) 个里面。
处理方法:思路一定要全,分清问题的主从关系。
-
注意一点,当 \(t> \max \{\lfloor\log_2 s\rfloor\}\) 的时候,此时倍增长度不再由 \(\log_2 s\) 决定,而是由 \(\log_2 n\) 决定。注意边界的细节。
-
注意当直线的方向向量为 \(\vec 0\) 的时候,这样的直线一般来说需要特判。
-
注意指数型生成函数的分母。
举个例子,如果要求 \(K![x^K]x^se^{nx}\),则最终的结果是 \(K^{\underline s}n^{K-s}\)。这是因为,最终取到的是 \([x^{K-s}]\),所以系数除的是 \((K-s)!\)!
其它
-
关于整体二分的写法问题。
这里由于我们不能简单地修改要求,所以在进入右区间的时候,我们必须保证左区间的边已经被加入。
如果写法是在离开区间时删除新加入的边,且在进入右区间之前将所有的左边的边加入,那么没有问题。
但是如果写法是在叶子的位置加入边,并且之后不删除,那么就必须保证叶子可以到达(现在外层相当于是遍历线段树的过程)。因此如果:
void Divide( const int qL, const int qR, const int vL, const int vR ){ if( vL > vR || qL > qR ) return ; ...}
那么就有问题(因为很有可能询问区间为空就返回,没有到叶子节点)。
-
DP 的转移:
比较下面的两种转移写法:
#define rep( i, a, b ) for( int i = (a) ; i <= (b) ; i ++ ) #define per( i, a, b ) for( int i = (a) ; i >= (b) ; i -- ) int h[][]; //other codes omitted. per( p, N, 0 ) per( q, N + 1, 0 ) { int tmp = 0; for( int k = 0, up = 1 ; i * k <= p && j * k <= q ; k ++ ) Upt( tmp, Mul( h[p - i * k][q - j * k], Mul( ifac[k], up ) ) ), up = Mul( up, Add( g[i][j], k ) ); h[p][q] = tmp; }
#define rep( i, a, b ) for( int i = (a) ; i <= (b) ; i ++ ) #define per( i, a, b ) for( int i = (a) ; i >= (b) ; i -- ) int h[][]; //other codes omitted. for( int k = 1, up = 1 ; i * k <= N && j * k <= N + 1 ; k ++ ) { up = Mul( up, Add( g[i][j], k - 1 ) ); for( int p = N - i * k ; ~ p ; p -- ) for( int q = N + 1 - j * k ; ~ q ; q -- ) Upt( h[p + i * k][q + j * k], Mul( Mul( ifac[k], up ), h[p][q] ) ); }
如果要求用
g[i][j]
将所有k
都转移一遍之后,h[][]
才能发生变动,那么第二种写法就有问题。这是因为,转移过程中较小的k
会对较大的k
造成影响,相当于是按照k
划分而不是按照g[i][j]
划分。总而言之,明确 DP 过程的阶段,同时找出合适的写法。
-
二分的结果:
如果在二分过程中计算方案,那么最终二分出来的答案是
l
而不是上一次代入的参数。因此,输出方案之前需要重新构造一遍!!! -
关于 WQS 二分。
我们二分的是切凸包的斜率。如果目标位置被共线的边卡住了,那么我们只能二分出正确的斜率,而多半不能找到正确的切点。但是我们只需要知道截距和斜率就可以算出答案,所以此时的答案不是切点的结果,而是目标位置在该斜率下的结果。
-
非确定性带通配符的字符串匹配,需要注意如果存在连续的与正则表达式.*
本质相同的通配符,则应当化到最简(即只有一个)之后再运算。或者说,处理复杂模式串最好要化到规范的、最简的形式。
-
推递归算法一定要注意边界,边界,边界!!!
这个问题有三个边界:\(a=0\) 时,\(\left\lfloor\frac{an+b}c\right\rfloor=0\) 时,\(k_2=0\) 时。
总之,当问题下降了一个梯级的时候,我们都可以当作边界处理,细心!
-
关于笛卡尔树启发式合并和启发式分裂的问题。
举个例子:「PA2014」Druzyny。
一般来说,用这类方法来分治优化 DP(或者卷积)的时候,需要注意最值在边界的情况。如果最值在边界,实际上可以直接切换一下划分的方式——把右块断点从 \(l\) 移动到 \(l+1\) 即可!
-
神奇的边界问题,比较有特殊性。
众所周知,一个 \(+1/-1\) 的随机序列,其前缀和最大值的期望是 \(O(\sqrt n)\) 的。这个性质可以用来优化 DP。
然而,我们不得不注意一下这个大 \(O\) 记号。这意味着,准确地取到 \(\lceil\sqrt n\rceil\) 有一定概率会出问题,当 \(n\) 比较小的时候尤其容易出问题。
所以,结论是,如果上界会影响正确性,则最好不要根据规模动态构造上界,实在是很危险。最好是使用一个比较稳定的固定上界。
计算几何
-
Minkowski 和,算出来的和不一定是严格凸包。
要么在算完之后再跑一遍凸包,要么直接在合并过程中就把共线情况判掉。