算法基础
基础算法
计算复杂度
复杂度在算法竞赛中对算法的选择有很大的帮助,利用复杂度可以简化思考,并帮助得到正确的算法。
一般来讲,将基础的运算操作都当成常数复杂度即 \(O(1)\),所以实际上在考虑的问题就是这种基础运算操作在数据规模极大的情况下的运算次数。
常见的复杂度有对数多项式,也就是常说的 \(polylog\),多项式也就是 \(poly\) 以及指数复杂度。
\(\Theta\) 和 \(O\) 符号
对于两个复杂度 \(f,g\),若 \(f=O(g)\) 则说明 \(f\) 在数据规模极大时不劣于 \(g\),比如说我们选择排序的复杂度可以是 \(O(n^2)\)。而 \(f=\Theta(g),g=\Theta(f)\) 即代表二者相距较近,只有常数的差别。
\(\widetilde O\) 符号
读作 soft \(O\),\(\widetilde O(g)=O(g\cdot polylog(g))\)。
从数据范围到复杂度
在具体应用中,根据不同的数据范围,可以猜测不同的算法复杂度。
uoj #751. 【UNR #6】神隐
有一棵未知的树,你需要对交互库进行若干次询问,来恢复这棵树的形态。每次询问时,你需要给出一个长度为 \(𝑛−1\) 的 \(01\) 向量 \(𝐴\),然后交互库会将每条满足 \(A_i=1\) 的边 \((𝑢_𝑖,𝑣_𝑖)\) 加入一张新的空图 \(𝐺\) 中,并返回其中每一个连通块(再删除这个图)。最后,你需要输出这棵树的所有边(顺序、方向任意)。
\(1\le n\le 2^{17}\)。询问次数不超过 \(20\) 次。
先思考子任务 1,2,4 怎么做。对于一条边,由于这是一棵树,所以切断他这两个点肯定不会联通。所以对于每个二进制位,询问这一位上是 \(0/1\) 的边的联通情况。那么这时假设是一条边,那么每一组询问一定都会恰好有一个是连通的,而如果不是树上的边,一定有一位是不是这样。这样就在 \(2\log n\) 次询问得到了连通情况,通过不同的实现可以有不同的复杂度。
一个常见的优化
这个算法的本质是给边进行二进制编号,并且使得每条存在的边,恰好将出现在询问中相同的次数。学过高中数学就知道 \(\binom{w}{k}\) 在 \(k=w/2\) 的时候最大,这个时候给每条边分配一个 \(w\) 位二进制数,恰好有 \(w/2\) 位是 \(1\),这个时候询问每一位是 \(1\) 的边即可。精细实现后即可通过本题。
这里考虑官方题解里一个更好的做法:
剥叶子,考虑叶子一定在一半的询问里单独在一个联通块。考虑在叶子的父亲处处理叶子,也就是在枚举到一个非叶子节点的时候,处理它的子树里还没有确定父亲的点。如果从下到上按深度处理就不会有问题。
LG4363 [九省联考 2018] 一双木棋 chess
一个 \(n\) 行 \(m\) 列的棋盘,棋盘上有两个数字 \(a_{i,j},b_{i,j}\),要想在一个位置落子,那么必须这个位置没有棋子且这个位置的左边和上边的所有格子都有棋子。
A 先手,B 后手。A 计算所有自己棋子上 \(a_{i,j}\) 的和,B 同理计算所有自己棋子位置 \(b_{i,j}\) 的和。两个人都希望自己的得分减去对方的得分得到的数尽量大,请求出双方都进行最优策略的情况下,最后 A 减去 B 得分的数值。
\(1\le n,m\le 10\).
乍一看很难,但是这个数据范围确实耐人寻味。考虑最暴力的状态?就是记录每一行有多少个位置已经被占了。
进行一个表的打,发现状态数竟然最多才 \(184756\),那还不直接压状态 dp?由于这是一个博弈 dp,得注意转移的方向性,得从后往前转移。
复杂度 \(O(n\cdot 184756)\)。
CF1779H Olympic Team Building
\(n\) 个人进行比赛,第 \(𝑖\) 个人的实力为 \(𝑠_𝑖\)。\(𝑛\) 为 \(2\) 的幂。比赛将一直进行到只剩一个人——这个人为赢家。对于每轮比赛:设当前有 \(𝑚\) 人。你要将这 \(𝑚\) 人分为人数相等的两队。队伍的实力为每个人的实力总和。如果两支队伍实力相等,你会选择谁获胜;否则,实力大的获胜。输的队伍中的每个人都被淘汰。问每个人能否可能成为赢家。
\(4\le 𝑛\le 32,1\le 𝑠_𝑖\le 10^{15}\).
\(n\le 32\),又是一个值得思考的数据范围。一般的指数难以通过了,又得看状态数了。
一个很直观的想法是枚举每个人能否成为最后的赢家,并且从一个人开始,不断地扩充这个集合。贪心?每次选择能选的集合中的最大的?再仔细观察一下,当集合大小为 \(16\) 时,只有一种选法了,成就成,不成拉到。当集合大小为 \(1\) 或者 \(8\) 时,显然是贪心最优。还有 \(2,4\) 的情况?一种想法是稍微多随机化+贪心枚举几种,一看这数据就不好造。那更好的做法呢?
大小为 \(2\) 时直接暴力枚举,就是 \(O(n^2)\),那大小为 \(4\) 的时候呢?暴力枚举,这样最终的复杂度大概是 \(O(n^6poly(n))\),显然爆炸。 一个显然的性质,如果集合 \(A\) 完全偏序了集合 \(B\),也就是说 \(A\) 从小到大排序,比 \(B\) 从小到大排序,每个元素都不小于,这个时候若 \(B\) 能赢,\(A\) 就一定能赢。
开始优化,第一步可以二分答案,然后大小为 \(2\) 时枚举一个,剩下一个贪心,大小为 \(4\) 时枚举三个,剩下一个贪心。这样复杂度大概为 \(O(n^4\log n)\)。然后是最后到了 \(8\) 个怎么贪心?还剩下 \(24\) 个,我们可以 meet in the middle!
细节很多,可以参考[代码](Submission #266553667 - Codeforces)。
uoj #823. 【UR #26】铁轨回收
给定连个数列 \(A_1\sim A_n,B_1\sim B_n\),从 \(i=1\sim n-1\) 依次遍历,随机一个 \(j\in [i+1,n]\),令 \(A_j=\min(A_j+A_i,B_j)\),对于每个 \(i\in[0,B_n]\) 求 \(A_n=i\) 概率模 \(998244353\)。
\(1\le n\le 50.\)
好难咕了,orz zsky。
一些常见的复杂度模型:
\(n\in [8,12]\) 可能是分拆数,大指数级别。
\(n\in [14,16]\) \(O(3^n),O(2^npoly(n))\) 级别。
\(n\in [18,20]\) \(\widetilde{O}(2^n)\)。
\(n\in [30,50]\) \(O(2^{n/2})\) 或者 \(O(n^4)\) 以上的多项式复杂度。
\(n\in [100,500]\),小常数 \(O(n^4)\) 或者 \(O(n^3)\)。
\(n\in [1000,2000]\) \(O(n^2\log n)\) 或者 \(O(\frac{n^3}{w})\)。
\(n=10^4\) \(O(n\sqrt{n}\log n)\)。
\(n=10^5\) \(O(n\log n),O(n\sqrt{n})\)。
\(n=10^6\) \(O(n\log n)\)。
\(n=10^9\) \(O(n^{2/3})\)。
\(n=10^{12}\) \(O(\sqrt{n})\)。
\(n=10^{18}\) 或更高,一般是数位 dp啥的。\(O(polylog(n))\)。
均摊复杂度和势能分析
最常见的均摊可能是 dsu,这个放到以后再说。
实际上均摊即为按大小分类,这样复杂度会是 \(O(\sum c_i=n)\) 的。
自动机类算法经常使用势能分析,这时一般会设计一个势能函数,根据势能的增减进行复杂度上界的估计,和均摊没有很大差别。
一些根号算法里也很常见,还有像 NOI2023D2T1。
NOI2023 贸易
一棵 \(2^n-1\) 个节点的树,每条边都是从儿子连向父亲。还有一些从祖先连向子树内后代的边。求任意两点间最短路径的和,如果不能达到则这个值为 \(0\)。
\(1\le n\le 18,1\le m\le 2^n\)。
分析一下每个 \(dis(x,y)\) 的构成,应该是 \(x\to lca(x,y)\to y\),前一部分是平凡的,对于后一部分,直接先继承父辈的距离,然后在子树内暴力 dijkstra,每个点会被计算祖先个数次,总的复杂度就是 \(O(n^22^n)\)。这题乍一看像是暴力其实复杂度均摊下来是对的。
不常见的数据范围
loj3400 storm
LG8292 [省选联考 2022] 卡牌
qoj 6372 dance
归约
也就是把一个问题一般化成已知最优解的问题,这在一些数据结构问题中尤为常见。
最常听的归约即为归约矩阵乘法。对于一个问题如果我们可以证明它不弱于 \(O(n*n)\) 的矩阵乘法,那么基本上就可以知道它的最优复杂度不能低于多少。而一般的做法就是把矩阵乘法问题抽象成我们要解决的问题,一般这个矩阵都是 \(O(\sqrt{n})\) 大小的,这样就证明了没有简单的 \(O(npolylog(n))\) 的做法。
分块复杂度
这类算法层出不穷,结合位运算可以达到意想不到的效果,具体可以参考分块的博客。
计算复杂性
复述 ycx 的论文(((
“本文中许多结论都是不加证明的,如果有兴趣的话可以自行查阅相关证明。”
咕。
模拟
对于代码能力的训练是很有效的(所有问题都是对想到的做法进行模拟)!
这里列举几个新题,对于代码设计的方向有较大的训练价值。
LG9499 「RiOI-2」change
有 \(n\) 种物品,每种物品的价值是 \(v_i\),个数为 \(c_i\),你可以用 \(x_i\) 个物品 \(i\) 换一个物品 \(i+1\),求最大的 \(\sum c_iv_i\),输出这个最大值对 \(998244353\) 取模后的结果。
\(1\le n\le 2\times 10^5.\)
高精度
另外,模拟里非常经典的部分就是高精度的计算,而一般高精度在写的过程中都会采取压位的策略。其实也很好理解,就是把原先的十进制可能换成了 \(10^9\) 进制甚至 \(10^{18}\) 进制。一般不会实现高精乘高精,这个时候交给 fft。
分治/倍增
分治,字面意思就是分而治之,是信息竞赛种很重要的一个思想。当一个问题可以分成两个子问题再合并时,就可以准备使用分治算法。而分治算法复杂度则可以通过主定理求得。
平面最近点对
noip2023
倍增是指将可以合并的信息进行 \(\log\) 拆分最后再合并。
RMQ
LCA
树状数组上倍增
分治思想很复杂(倍增也是),我不想在这里过多提及,所以会放到以后的笔记中去。
二分、三分
排序&数据降维技巧
前缀和&差分
差分是前缀和的逆运算,通过前缀和,我们可以快速获得区间信息和矩阵信息。
Ref
《浅谈复杂度及其在解决问题上的应用》杨敏行
《浅谈计算理论和OI中的难解问题》闫陈效