对我学过的算法的一些总结

对我学过的算法的一些总结

随缘更新,希望能帮到你,不过肯定能帮到我就是了。

\(\LaTeX\) 写的不是很严谨,其中的 “\(=\)” 具体是等于还是赋值就自己悟吧,我懒得改。

给出的模板都是我自己学习时写的,存储在 Github 上,中国大陆部分网络环境无法直接访问,可以看我的 镜像仓库

看右边的目录。


不知道该怎么归类的算法

二分

对具有单调性的东西快速查找合法边界。

\(O(log_n)\)模板oiwiki.

二分查找

对具有单调性的数组操作,每次以 \(mid = {l+r\over 2}\) 作为边界将搜索范围缩小 \(len\over 2\),细节非常多。

使用 STL 的容器实现

查找左边界:l = lower_bound(a.begin(), a.end(), x) - a.begin() - 1

查找右边界:r = upper_bound(a.begin(), a.end(), x) - a.begin()

是否存在:bool flag = (a[l] == x)

二分答案

如果答案具有单调性(有一个明确的“合法 & 不合法”的分界点,一侧所有都合法另一侧所有都不合法),就对答案进行二分,直到找到最小(最大)的合法答案。

高精度

对很大很大的数字做四则运算。

总论

oiwiki, 模板

一般来说,高精度数字用 string 读入,然后用 vector<int> 逆序存储,输出答案再逆序输出就好了。

计算一般都是模仿现实对应运算时竖式运算的做法,下面会具体介绍。

高精度加法

用一个变量 \(t\) 作为当前列的答案和进位,将 \(t\mod 10\) 作为真实的答案,并 \(t \leftarrow t \div 10\) 保留到下一位作为进位。

如果 \(t\) 还有剩的话就说明还有最高位的一个进位啊,push_back 即可。

高精度减法

需要处理减出负数的问题,所以要先对比 \(A\)\(B\) 的大小,按以下顺序处理:

  • 谁更长谁就更大。(呃呃
  • 逆序遍历每一位,当前位不一样的时候谁大谁就更大。
  • 都一样就按 \(A\) 大来算。

如果 \(B\) 大,先给一个负号。

用一个变量 \(t\) 作为下一位对自己的借位,所以先让 \(A\) 减去自己,也就是减去下一位对自己的借位。

然后再减去 \(B\), 注意这里可能有负数,所以这一列的答案是 \((t+10)\mod 10\).

如果是负数的话,说明有结尾,令 \(t \leftarrow 1\) 后继续处理,否则就是 \(0\).

做完清前导零。

高精度乘法

高精乘低精

同样以 \(t\) 作为当前列的答案和进位啊。不过因为要考虑最高位进位所以要在循环范围加一个 || t.

然后就是模拟竖式乘法,\(t \leftarrow A_i \times b\),然后 \(t \mod 10\) 是当前列答案,\(t \leftarrow t \div 10\) 顺延到下一位作为进位。

做完清前导零。

高精度除法

高精除低精

\(r\) 作为余数传进去,顺位模拟除法(除法要从高位从低位除),处理每一列的时候模拟余数的行为,即 \(r \leftarrow r \times 10 + A_i\). 然后再把 \(r \div B\) 作为当前列的答案,并 \(r \leftarrow r \mod B\) 作为当前列的余数顺延到下一位。

做完记得 reverse 一下,然后清前导零。


数据结构

存储数据的结构。

栈和队列太简单不想写。

并查集

快速查询两个点之间的连通性,整个图连通块个数与大小。

复杂度不会算,模板oiwiki.
记录每个点的父亲,初始每个点的父亲都是自己,合并时将一个点的祖宗的父亲从自己设置为另一个点的祖宗,查询联通性时查询祖宗是否相等,初始连通块个数为 \(n\),每合并一次连通块个数 \(-1\),连通块大小在合并时从被合并的集合的祖宗加给合并的集合的祖宗。

前缀和与差分

oiwiki.

前缀和

快速查询离线数组区间和。

预处理 \(O(n)\),查询 \(O(1)\)模板
已有一个数组 \(arr\),维护一个数组 \(pre\) 满足 \(pre_i = pre_{i-1}+arr_i\)(即“数组的前 \(n\) 项和”), \(\sum_{i=l}^{r}arr_i\) 的答案即为 \(pre_r - pre_{l-1}\)

二维前缀和

就是前缀和的二维形式。预处理 \(O(n)\),查询 \(O(1)\)模板
原数组 \(a_{i, j}\),前缀和数组 \(s_{i, j}\),基于容斥定理可得:

  • 预处理:\(s_{i,j} = a_{i,j} + s_{i-1,j} + s_{i,j-1} - s_{i-1, j-1}\)
  • 查询:\(\sum\limits_{i=x_1}^{x_2} \sum\limits_{j=y_1}^{y_2} a_{i,j} = s_{x_2,y_2} - s_{x_1-1,y_2} - s_{x_2,y_1-1} + s_{x_1-1,y_1-1}\)

差分

快速区间加值。

修改 \(O(1)\),查询 \(O(n)\)模板
对于数组 \(arr\),它的差分数组为 \(s_i=arr_{i}-arr_{i-1}\),对区间 \([l,r]\) 加上一个数字 \(a\) 即为 \(s_l \leftarrow s_l+a,s_r \leftarrow s_{r+1}-a\),查询时对 \(s\) 做一遍前缀和即可。

二维差分

二维的差分,修改 \(O(1)\),查询 \(O(n^2)\)模板
原数组 \(a_{i,j}\),差分数组 \(s_{i,j}\),反之就是可得:

  • 预处理就是单点修改
  • 修改:
    • \(s_{x_1,y_1} += c\)
    • \(s_{x_2+1,y_1} -= c\)
    • \(s_{x_1, y_2+1} -= c\)
    • \(s_{x_2+1, y_2+1} += c\)
  • 查询做一遍二维前缀和。

哈希表,堆

我不会。

链式前向星

存图。

模板
我也不知道这么写为什么对。

树状数组

快速 {单点修改区间查询} 或 {区间修改单点查询}。

修改O(\log_n),查询O(\log_n),很好写,oiwiki点修区查模板区修点查模板
类似线段树的思想,每个树状数组中的下标都有一个管辖区间,对于 \(s_i\) 来说,管辖区间的长度为 \(2^{lowbit_x}\)。这里的 \(lowbit_x\) 指的是 \(x\) 二进制表示最低位所在的二进制位数,如 \(lowbit_{01001000_{(2)}}\) 即为 \(00001000_{(2)}\)

点修区查

  • 修改:从 \(x\) 开始跳到 \(n\),每次 \(x \leftarrow x + lowbit_x\) 并将树状数组 \(s\) 加上 \(c\)
  • 查询:从 \(x\) 开始跳到 \(0\),每次 \(x \leftarrow x - lowbit_x\) 并将答案加上 \(s_x\),再利用前缀和的思想,\([l,r]\) 的和即为 \(query_r - query_{l-1}\).

区修点查

  • 修改:把树状数组 \(s_i\) 作为对于原数组 \(a_i\) 的偏移量数组(即 \(\Delta\)),用差分的思想去更改,由于树状数组的特性,只需要对两个区间边界(\(l, r+1\))修改就可以实现对区间的修改,最后 \(x\) 的值即为 \(x + query_x\)

线段树

可以高效维护区间信息的数据结构。

修改 / 查询 \(O(\log_n)\)oiwiki, 模板

线段树总论

线段树是一棵二叉树,每个节点都存储着一个区间的信息,每个非叶节点的两个子节点都是自己自身长度的一半,故树高在 \(\log_n\) 级别。

线段树分为建树(build()),更新(pushup()),更改(change()),查询(query())和下传(pushdown())四个操作。

更新

用左右子节点的信息更新到当前节点。

建树

对于一个非叶节点,它的左子节点的编号为 \(u\times 2\),右子节点为\(u\times 2 + 1\),当 \(l = r\) 时把当前子节点的一些信息改成自己。

然后递归建立左右子树,建立完更新当前节点。

下传

对于区间修改,可以暂存在当前节点的信息可以用懒标记维护。

下传即为将当前节点的懒标记向左右子树扩散的操作,下传完成后将当前节点的懒标记清除。

更改

下传懒标记,递归更改左右子树,当当前子树完全被更改区间包含时直接更改,然后更新当前子节点。

查询

如果当前子树完全被更改区间包含直接返回区间值,否则先下传当前节点的懒标记,然后递归查询左右子树。

图论

研究离散数学的图上问题。

以下默认 \(n\) 为点数,\(m\) 为边数。

DFS

就是深度优先搜索。

复杂度我不知道,反正巨大的,oiwiki图论oiwiki搜索

一般有一个明确的边界条件,搜出来到这个边界条件的所有可能就可以,大概长下面这样:

dfs(当前步数)
  if(超过边界了)
    统计答案
    return;
 
  for 所有相邻情况
    if(不合法 || 搜过了)
      continue;
    st[当前状态] = 1;
    dfs(当前步数 + 1);
    st[当前状态] = 0;

大概就长这样啊,自己悟吧。

最短路

在图中一个点到另一个点的最短路径,有时也可以抽象成一个状态达到另一个状态的最小代价。

BFS

无边权时查找达到某个状态(点)的最少步骤(最短路径)。

\(O(n)\)oiwiki图论oiwiki搜索

设定好(可以是多个)初始状态,维护一个队列,将所有初始状态压入队列,每次拿出队头,遍历所有这个队头可以扩展到的状态并压入队列,因为没有边权所以 \(dist_j = dist_t + 1\),如果 \(dist_j \ne 0\) 即这个点被搜过,跳过即可。

没找着我写的模板,所以不放。

Dijkstra

非负权图单源最短路。

\(O(n\log_n+m)\)oiwiki模板

维护一个“已确定最短路”的集合 \(S\),初始化将 \(dist_{sta} = 0\),其余为 \(+\infty\),一开始所有点都不在 \(S\) 中,重复将不在 \(S\)\(dist_i\) 最小的 \(i\) 取出放入 \(S\),并从 \(i\) 出发将所有与 \(i\) 相邻且在 \(T\) 集合中的点松弛,直到 \(S\) 集合中包含所有点。

具体实现中,用一个 \(bool\) 数组维护集合 \(S\),用单调队列优化“寻找最小 \(dist_i\)”的操作。

SPFA

超快的求最短路径的算法死了,可带负权图单源最短路或负环。

\(O(nm)\),有时常数巨大*,oiwiki最短路模板负环模板deque优化最短路模板

核心思想是将所有被松弛过的点拿来松弛其他点,用一个队列维护,同时用一个 \(bool\) 数组维护点是否在队列,初始将 \(sta\) 压入队列,然后随便做做就做出来了。

最短路存在时,一条最短路的边数最多为 \(n-1\),因此当松弛 \(n\) 次时说明最短路不存在且有负环。

Floyd

可带负权图任意两点之间最短路。

\(O(n^3)\) 但常数很小,暴力有时能草过去 \(n=700\)oiwiki模板

我也不知道这么写为什么是对的,看 oiwiki 吧,以后学明白了可能会回来补。

最小生成树

在一个图中生成一棵树,满足树的边权和最小。

Prim

我不会。

Kruskal

用贪心思想和并查集实现最小生成树,\(O(m\log_m)\)oiwiki模板

把所有边按从小到大排序,然后从小到大加入边。

“当前点是否已在最小生成树内”用并查集维护,以及这个思想是贪心。

数学

研究很多数学问题。

分解质因数

把一个合数分解成若干个质因数的乘积的形式。

基于算数基本定理,\(O(\sqrt{n})\)oiwiki模板

\(i=2\) 循环到 \(i=\sqrt{n}\),当 \(n\mid i\)\(i\) 就为 \(n\) 的一个质因子,将 \(n\) 除干净(\(n \nmid i\))后继续操作,操作结束后如果 \(n \ne 1\) 说明当前的 \(n\) 自己也是质因子之一。

我不会证,反之这么做就是对的。

线性筛 / 欧拉筛

朴素筛和埃式筛没吊用而且我不会所以不写。

快速求出从 \(2\)\(n\) 有多少个质数。

\(O(n)\)oiwiki模板

关键思想就是让每一个合数都被它最小的质因子筛掉。

维护一个素数列表 \(primes\) 和 素数标记数组 \(not\_ prime\),从 \(i=2\) 循环到 \(i=n\),如果 \(i\) 不是合数就压入 \(primes\),并且对所有 \(i \times pri_j\) 标记为合数,同时如果 \(i \mid pri_j\) 说明 \(i\) 之前已经被 \(pri_j\) 筛过了,直接开始处理下一 \(i\),具体证明看 oiwiki 即可。

最大公约数(欧几里得算法)

__gcd()

快速幂

快速求出 \(a^b \bmod p\)

\(O(\log_b)\)oiwiki模板

预处理出 \(a^{2^0}, a^{2^1}, a^{2^2}\) ······ \(a^{2^{\log_b}}\),将 \(b\) 二进制分解,如果 \(b\) 的第 \(i\) 位是 \(1\) 就让答案 \(res\) 乘上 \(a^{2^i} \bmod p\),将 \(b\) 的所有二进制位遍历完后 \(res\) 即为 \(a^b \bmod p\)

快速幂求逆元

快速求任意 \(a\)\(\bmod p\)\(p\) 为质数意义下的逆元。

\(O(log_{p-2})\)oiwiki模板

费马小定理可知,\(a\times a^{p-2} = a^{p-1} \equiv 1 \pmod p\),根据逆元唯一性可知,此时 \(a^{p-2}\) 就是 \(a\)\(\bmod p\) 意义下的逆元。

扩展欧几里得算法(exgcd)

\(ax+by=\gcd(a,b)\)(即二元一次不定方程)的一组可行解。

\(O(\log_n)\)oiwiki模板参考文献

由欧几里得定理得 \(ax+by=\gcd(a,b) =\gcd(b, a \mod b)\), 设 \(x',y'\)\(bx+(a\bmod b)y=\gcd(b,a\bmod b)\) 一组解,则有:

\[\begin{align} ax+by &=\gcd(b,a\bmod b)\nonumber\\ &=bx'+(a\bmod b)y'\nonumber\\ &=bx'+(a-\left \lfloor \frac{a}{b} \right \rfloor \times b)y'\nonumber\\ &=bx'+ay'-\left \lfloor \frac{a}{b} \right \rfloor \times by'\nonumber\\ &=ay'+b(x'-\left \lfloor \frac{a}{b} \right \rfloor \times y')\nonumber \end{align} \]

其中,$ a\bmod b = a- \left \lfloor \frac{a}{b} \right \rfloor \times b $ 可以这么理解:\(a \bmod b\) 等价于 \(a = q \times b + r\),其中 \(r\) 为余数,\(q\in \mathbb{N}^+\),易知 \(q = \left \lfloor \frac{a}{b} \right \rfloor\),移项就得证了。

于是,我们就得到了一组解,然后递归递归写写就出来了,多看看板子就行,具体可以看上面的参考文献。

反正这个算法最核心的公式就是这个了,具体裴蜀定理什么的就是说这个东西有解,而且解当且仅当 \(ans \mid gcd(a,b)\),具体为啥我也不会证,看上面那个参考文献就行,那哥们写的真牛逼。

求组合数

就是,求组合数。

帕斯卡定理求组合数

预处理 \(O(n^2)\) 查询 \(O(1)\),公式为 \(C_{n}^{m} = C_{n-1}^{m} + C_{n-1}^{m-1}\),注意到计算 \(C_n^m\) 时用到的下标 \(i, j\) 都满足 \(i,j \le n,m\),所以从小到大循环可能的 \(n\)\(m\) 递推即可,模板

参考数据规模:\(1\le n \le 2e3\), \(1\)s.

组合数公式求组合数

预处理 \(O(a \times \log_p)\) 查询 \(O(1)\),从组合数公式 \(C_{n}^{m} = \frac{n!}{m!(n-m)!}\) 出发,由于是求模意义下组合数所以要将所有可能的阶乘(分子)和阶乘的逆元(即分母)求出来,直接套公式即可,模板

参考数据规模:\(1\le query \le 1e4\), \(1\le a,b \le 1e5\), \(1\)s.

Lucas 定理求组合数

Lucas 定理:\(C_n^k \equiv C_{n \mod p}^{k \mod p} \times C_{\lfloor n/p \rfloor}^{\lfloor k/p \rfloor} \pmod{p}\),适用于模数不固定且不算太大(\(p \approx 1e6\))时。

注意到 \(a,b\) 可能巨大,所以考虑递归求解。因为 \(p\) 较小所以 \(n,k \mod p\) 一定是在一个较小的范围内,可以直接用一个 \(O(n)\) 的循环求解出答案,而另一项可能较大,可以循环调用自己,当 \(a, b < p\) 时再循环求解组合数并回溯。

参考数据规模:\(1 \le a,b \le 1e18\), \(1 \le p \le 1e6\), \(1\)s.

动态规划

一种把原问题划分为若干相对简单的子问题进行递推求解的算法。

动态规划总论

动态规划的本质就是“把大问题分解为可以快速求解的小问题,再由已知的小问题回溯求解出大问题”的算法,这是很难直接想出来做法的,所以要分成下面几步:

  • 用几个合适的下标表示出来现在状态的集合,这个表示方法应该能表示所有可能的情况。
  • 思考这个集合如何从已知的子集合中转移过来,也就是怎么样去保证在求解当前集合的时候需要的几个子集合的答案都已知。
  • 思考转移顺序。

线性动态规划

有明显线性推导关系的动态规划。

没有模板,现推去吧。

背包动态规划

可以被抽象为往背包里装物品的问题,算是一种特殊的线性动态规划,但是实在是太重要了所以单独开了一个二级标题。

0-1 背包模型

\(n\) 件重量 \(v_i\), 价值 \(w_i\) 的物品装到容量是 \(m\) 的背包中,每件物品装一次,求最大价值,\(O(nm)\)oiwiki, 模板.
定义 \(f_{i, j}\) 为只放前 \(i\) 个物品,最大容量是 \(j\) 时的最大价值,则有:

\[f_{i,j} = \max(f_{i-1, j}, f_{i-1, j-v_i}+w_i) \]

答案为 \(f_{n, m}\).

发现第一维可以滚动数组优化,则有:

\[f_{j} = \max(f_{j}, f_{j-v_i}+w_i) \]

\(j\) 从后往前枚举,答案为 \(f_{m}\).

完全背包模型

\(n\) 件重量 \(v_i\), 价值 \(w_i\) 的物品装到容量是 \(m\) 的背包中,每件物品可以装无限次,求最大价值,\(O(nm)\),[oiwiki](https://oi-wiki.org

定义 \(f_{i,j}\) 为在前 \(i\) 个物品里中选,最大容量是 \(j\), 第 \(i\) 个物品选 \(k\) 个的最大价值,则有:

\[f_{i, j} = \max_{k=0}^{k*v_i <= j}(f_{i-1, j-k\times v_i}+w_i\times k) \]

时间复杂度 \(O(n^2m)\),只考虑 \(f_{i, j}\),做以下推导:

\[f_{i, j} = \max(f_{i-1, j}, f_{i-1, j-v}+w, f_{i-1, j-2v}+2w, f_{i-1, j-3v}+3w, ......) \]

\[f_{i, j-v} = \max(\qquad f_{i-1, j-v},\qquad\; f_{i-1, j-2v}+w\;, f_{i-1, j-3v}+2w,......) \]

容易发现,原状态转移方程等价于下式:

\[f_{i, j} = \max(f_{i-1, j}, f_{i, j-v_i+w_i}) \]

套上滚动数组优化,最终有:

\[f_{j} = \max(f_j, f_{j-v_i}+w_i) \]

\(j\) 从前往后枚举,答案为 \(f_m\).

多重背包模型

\(n\) 件重量 \(v_i\), 价值 \(w_i\), 数量 \(s_i\) 的物品装到容量是 \(m\) 的背包中,求最大价值,\(O(nm\bar s)\)oiwiki模板.

定义 \(f_{i, j}\) 为在前 \(i\) 个物品中选,容量最大为 \(j\),第 \(i\) 个物品选了 \(k\) 个的最大价值,则有:

\[f_{i, j} = \max_{k=0}^{k<=s_i, k\times v_i<=j}(f_{i-1, j-k\times v_i}+w_i\times k) \]

时间复杂度 \(O(nm\bar s)\),考虑以下推导:

本问题的物品件数是固定的,每件物品可选的件数为 \([0,s_i]\),所以考虑二进制拆分 \(s_i\)\(\log_{s_i}\) 个物品,再对这一堆物品做一遍 0-1 背包就可以了,易证可以组合出所有可能的选法,时间复杂度 \(O(nm\log_{\bar s})\)

分组背包模型

\(n\) 组物品,每组物品只能选 \(1\) 个装到容量为 \(m\) 的背包,求最大价值,\(O(nms)\)oiwiki模板.

对每组做一遍 0-1 背包就行,懒得写。

混合背包模型

给你 \(n\) 个物品,一些只能拿 \(1\) 个,一些能拿无限个,一些能拿 \(s_i\) 个,放进容量为 \(m\) 的背包里,求最大价值。

循环物品的时候套这三个背包的代码即可,太傻逼了不想写。

区间与环形动态规划

在一条或一个上对区间进行动态规划。

oiwiki.

区间动态规划有明显的能将问题分解为两个子问题合并在一起的特征,即可合并性,这个词是我自己发明的。

一般用下面的方法来求解:枚举已经求解完的子问题,即枚举断点 \(k\),答案为 \(\sum_{k = l}^{r}f_{l,k}+f_{k+1,r}+sum_{l,r}\),其中 \(sum_{l,r}\) 为 l ~ r 合并的代价,为 \(\sum_{i=l}^{r}a_i\) 。一般用前缀和来处理,即 \(sum_{l,r} = s_r - s_{l-1}\) ,常见代码形式如下。

for 区间长度
 for 左端点
  int r = l + n - 1;
  for 区间断点 l to r
   求解f

对于环,一般是把原链复制一遍拼接到链尾,因为答案的合并路径不超过 \(1\) ~ \(n\) 中的某个断点,所以枚举区间长度的时候仍然枚举到 \(n\),这样就可以在 \(O(n^3)\) 的时间复杂度下求解环上区间动态规划问题了。

树形动态规划

在树上进行的动态规划

oiwiki.

一般以根作为划分依据,即:
\(f_{u,k}\) 为以 \(u\) 为根的子树的最优解,\(k\) 为一些状态。

转移顺序一般是 DFS,即自下向上转移,这也符合动态规划的特征。

边界为叶子节点。

树上背包我不会,换根 dp 我也不会。

状态压缩动态规划

将状态转化为二进制记录在下标中的动态规划。

oiwiki.

特点是 \(n\) 极小,且状态可以很方便地用二进制来表示。

一般来说状态很多,但合法的状态不多,合法的转移状态更不多,所以我们可以预处理出所有的合法状态和所有的合法转移。

当然,这个思路也不能求解所有的状压 dp 问题,还是要具体情况具体推导,但是“把状态转化为二进制表示”的思想是不变的。

一般来说,状压 dp 的求解是这样的:

for 所有状态
 if(状态合法)
  state.push_back(i)

for 当前状态 to 所有状态
 for 下一状态 to 所有状态
  if(转移合法)
   legal[i].push_back(j)

求解dp

数位动态规划

对数字的每一位进行动态规划。

提高组不考不想写。

字符串

KMP

Manacher

高效的求所有位为中心的回文子串和最长回文子串的算法。

\(O(n)\), oiwiki模板

核心思想是由已知的回文串求新的回文串,设 \(r\) 为已知回文串的最右边界,\(c\) 为已知最大回文串的中心,\(i\) 为当前求解的回文串中心,\(p_i\) 为以 \(i\) 为中心的最大回文串长度。

\(i < r\)\(p_i\) 初始化为以它与 \(c\) 对称的点的 \(p_j\),如果 \(i\) 太靠近 \(r\) 了就是 \(r-i\),即为:if (i < r) p[i] = min(p[c * 2 - i], r - i);,否则 \(p_i \leftarrow 1\),也就是暴力。

然后向左右暴力扩展,如果 \(i\) 的右端点(\(i+p_i\))大于 \(r\) 了,则更新 \(r\) 和中心 \(c\).

为了统一操作,在原字符串中插入无关字符,如“aaa” 变为 “#a#a#a#”.

posted @ 2025-10-12 11:22  Misaka2298  阅读(11)  评论(0)    收藏  举报