10circle 算法学习笔记
实际上……还有很多没补的。以后会补的(确信(
也会有一些技巧之类的。
可能需要一些 OI 基础。
不保证看完就能明白,但是会尽力让人能看明白的。
最近更新:分拆成了一些小项。
分拆中,此页面仅作汇总。
- 平衡树-Treap 及其拓展
数据结构是好的。平衡树一篇就已经巨长所以单独拆出来。 - 分治宣讲
一个讲稿,感觉挺好的就放博客上来。 - [树形数据结构及拓展]()
数据结构是好的。树形也不只是树。含线段树、左偏树、可持久化数据结构、ST 表、树状数组、KDTree、李超树。 - [图论算法]()
含欧拉回路、欧拉路径、Tarjan 算法、拓扑排序、网络流算法、有向图最小生成树(最小树形图)、次小生成树、k 短路和图匹配算法。 - [经典图论模型与图的性质]()
如题,含 2-SAT、差分约束、二分图的各个结论、网络流经典建图和最大流最小割定理。 - [OI 中的字符串算法及自动机]()
有各种字符串算法和自动机相关题目,含有 Hash、Trie 树、AC 自动机、后缀数组、后缀自动机、回文自动机、kmp、exkmp(Z 函数)、Manacher 和自动机上 dp。 - [组合数学]()
含容斥原理、各种数、Lucas 定理、各种组合等式、生成函数、形式幂级数和例题。 - [线性代数]()
含向量、矩阵基础、矩阵快速幂、高斯消元、行列式和二维向量的各种运算和用处,大概也算是二维计算几何基础。 - [OI 中组合数学常见知识]()
含矩阵树定理、Prufer 序列、LGV 引理。 - [树的性质及树相关算法]()
含树上启发式合并、点分治、括号序列和树的关系、树链剖分、Euler Tour Tree。 - [群论基础和群在计数中的应用]()
群是好的,群在旋转而又跳跃。 - [数论基础]()
含数论函数简介、狄利克雷卷积、莫比乌斯反演和原根。建议和群论基础一起读。 - [数论算法]()
含线性筛、线性基、中国剩余定理、数论分块、BSGS、欧几里德算法(gcd 和 exgcd)。 - [复杂 dp 及优化]()
含斜率优化(凸包/李超树)、四边形不等式、数据结构优化 dp、插头 dp 等。 - [多项式及其算法]()
含拉格朗日插值、FFT & NTT 等。 - [杂项和技巧]()
含 set 维护颜色连续段、WQS 二分、分数规划、莫队、CDQ 分治、整体二分、分块、根号分治、打表、找规律、扫描线、表达式求值、搜索和随机算法等。 - [博弈论]()
- [二维计算几何]()
含凸包、旋转卡壳、半平面交等。
原先的内容见下。
实际上……还有很多没补的。以后会补的(确信(
也会有一些技巧之类的。
由于太长了,加载太慢,准备分拆。
正在改版中……应该很快就能更新出来。
最近更新:更新了拓展欧几里德算法。
WQS 二分
树上启发式合并(DSU on Tree)
CF1009F
欧拉回路
连通性问题
2-SAT
网络流
二分图模型
KMP
拓展 KMP
AC 自动机
JUMP JUMP!!
这个东西是用来解决一堆字符串共同执行某个操作的(比如查询在另一个字符串中出现的次数)。
首先对这一堆字符串建一个字典树。字典树上一个节点就是一个状态,可以表示从根到它的边所连起来形成的字符串,即某个(些)串的一个前缀。下文 均表示某个状态。定义 为 在字典树上的父亲状态, 为 状态再接 后的状态。
AC 自动机中最重要的就是 指针了。 就指向 所表示的字符串在本字典树内的最长后缀的状态。感觉自己不会说话,如果不明白可以看 OI-wiki 中对 指针的说明。
构建 指针需要从短到长构建。
若 存在,则 ,即本字符串与最长后缀同时增加了字符 ,显然还是最长后缀。
其余情况,需要不断向前跳。由最长后缀跳到最长后缀的最长后缀……直到存在 的边。这样可以从长到短遍历所有在字典树中的本字符串的后缀。如果始终没有 的边,则 就是空串。
但是这样不好写而且慢,需要优化。跳,可以路径压缩。因为不存在 ,所以可以把它设成跳完的,即 ,相当于直接扯到存在 的地方了。
查询就沿字典树游走。由于已经进行了路径压缩,不用判断到结尾要跳 。沿 累加某个状态出现了多少次。就是先枚举子串后端点,再沿 跳前端点。但是这样不好,慢。需要先标记一下,最后再累加。这样就是线性的了。
Code
Build fail:
queue<int>q;
for(int i=0;i<26;i++)if(tr[0][i])q.push(tr[0][i]);
while(!q.empty()){
int u=q.front();q.pop();
for(int i=0;i<26;i++){
if(tr[u][i])f[tr[u][i]]=tr[f[u]][i],q.push(tr[u][i]);
else tr[u][i]=tr[f[u]][i];
}
}
query:
int u=0;
for(int i=0;s[i];i++){
u=tr[u][s[i]-'a'];
ct[u]++;
}
例题:洛谷的板子、[BJOI2017] 魔法咒语(矩阵快速幂、自动机上 dp)、[JSOI2007] 文本生成器(自动机上 dp)
后缀数组
后缀自动机
组合数学
卡特兰数及应用
插板法
容斥
线性代数
依铃也不知道为啥叫“线性代数”……反正是这个名(
向量
这里面首先有“向量”这个概念。既有大小又有方向的量称为向量。但是在 OI 中除了 FFT 好像都可以只用坐标表示向量。向量记作 或 。
比如一个二维向量,就可以表示为 的样子。就相当于变化量。当然向量也有多维的,于是 vector 就出现了。一个 维的向量可以用 表示。用坐标理解这个东西可能好理解些。零向量没有大小,自然也不确定方向,相当于一个点,用 表示,其 维坐标都是 。
向量是可以相加的,变化量可以在这里体现。比如 ,就是每一维相加。相减类似,也是每一维相减,可以理解为 。加的几何意义就是从原点开始按照 平移之后再按照 平移。比如一个点 按二维向量 平移后就会变成 。这个东西可能有图更好理解些……但不好弄(。可以自己画几个图感受一下(,依铃不太会说(。不过这东西高中会学的。
向量还可以数乘,就是直接放大一些倍。,然后方向就可能相反,也可能和原来一样。
两个向量共线就是说这两个线段平行(向量是可以任意平移的,就和线段一样,端点变了但是大小方向不变也是同一个向量),用数乘表示就是两个非零向量 与 共线 有唯一实数 ,使得 。
向量的模就是向量的长度也就是向量的大小(明明是一个东西但它就是有一堆词),用 表示。。(这就是两点间距离公式,应该还好,会勾股定理就能推出来)。
矩阵及快速幂矩阵的设计
还有一个东西,叫矩阵。
这是一个 行 列的矩阵,你可以当它是一个二维数组。这里依铃用 表示 的行数, 表示 的列数, 表示 中第 行第 列的元素。
矩阵的加减要求 ,即运算的两个矩阵行数和列数相等,然后逐个元素进行。
矩阵的乘法要求 ,即第一个矩阵的列数等于第二个矩阵的行数。然后,设结果为 ,那么 。显然 。通俗的讲,在矩阵乘法中, 矩阵的第 行第 列的数,就是由 第 行的 个数与 第 列的 个数分别相乘再相加得到的。矩阵乘法没有交换律,但满足结合律。
接下来是用处,矩阵加速递推。有一个递推式,其中 给定:
于是思考将它优化至 ,这需要构造一个初始矩阵 和一个转移矩阵 。 需要包含 ,可以写做 。而 需要将 转移到 ,可以写做:
然后答案矩阵为 。它可以写作 。注意可以优化的递推式只有常系数(好像是的)。快速构造的方法就是
然后往里面填数就行。左侧就是矩阵乘法中那个 ,而转移后就是上面的一行,也就是要求的 。然后快速幂就行。
矩阵也可以表示变换,方式同上,不细说了。
例题:Luogu 板子、[NOI2011] 兔农,[NOI2021] 密码箱(还用平衡树)。好了差不多讲完了,可以写题了(?。
模板代码
struct matrix{//为方便起见,程序中下标从 0 开始。
ll a[N][N];
matrix(int ag=0){
memset(a,0,sizeof a);
if(ag)for(int i=0;i<N;i++)a[i][i]=1;
}
};
matrix operator*(const matrix &a, const matrix &b){//由于内存不连续访问常数较大
matrix c;
for(int i=0;i<N;i++){
for(int j=0;j<N;j++){
for(int k=0;k<N;k++){
c.a[i][j]=(c.a[i][j]+a.a[i][k]*b[k][j])%mod;
}
}
}
return c;
}
/*matrix operator*(const matrix &a, const matrix &b){//常数较小
//自己思考x((
}*/
matrix pw(matrix a, ll k){
matrix ans(1);
while(k){
if(k&1)ans=ans*a;
a=a*a;k>>=1;
}
return ans;
}
分数规划
高斯消元
中国剩余定理
拉格朗日插值
大纲里没有但是它考了。离谱。
莫比乌斯反演应用
数论分块
群及其应用
进行运算的东西和结果的类型相同(类型的概念很模糊,只是为了感性理解。如果是群或类似概念,就是在一个集合中)的运算是有封闭性的。比如 在实数范围内就是没有封闭性的。
为了更好的探究运算的性质,数学家们引入了群。群由一个集合和一个集合上的运算构成。如果二元运算 (泛指运算)在集合 中有封闭性和结合律(注意并不用交换律),那么它们就构成了半群 。注意这还不是群,只是半群。
群还需要单位元。单位元一般用 表示。它的定义是 。 的意思就如同它的 LaTeX 名一般,是“对于所有”的意思。 的意思也如同它的 LaTeX 名一般,是表示一个元素属于一个集合的意思。为啥叫单位元呢,首先这是个元素,而且它就像是单位 一样,任何数和它做运算都不会改变。含有单位元的半群叫做含幺半群。
思考 1:会不会存在两个单位元呢?可以试着写出证明。tip:。既是 又是 ,所以只有一个单位元。
思考 2:若 ,,那么 和 是不是相同呢?
证: (第一步,就是说把 替换为 )
(第二步)
(第三步)(非常简单是不是(
也许需要举个例子……?比如 就是一个含幺半群(也是一个群),其中 指整数集。对,单位元 。但是 是含幺半群却不是群,因为逆元(下面讲)可能不是整数,不属于整数集合 。
然而含幺半群还不是群,群中的每一个元素还有它的逆元。 的逆元一般用 表示。同时,在下面的叙述中, 都指逆元。逆元的定义是 。注意,下面两个都是在含幺半群的条件下的证明,不是所有元素都有逆元。
思考 3:逆元可能有两个吗?
思考 4:若 ,那么 和 是否相同?(显然下面这个问题严格强于上面的,因此只证下面的了。
证:。
群要求 。 的意思就和它的 LaTeX 名一样,是“存在一个”的意思。 全称 subject to, 意思是使得…满足…。于是群的定义就讲完了。
群中一个元素和它自己做 次运算,即 ,表示为 。看着和幂很像,也可以以幂理解,但不一样。那么,很显然的,这个有整数幂的性质,包括负数。
比如一个长度为 的排列(置换)和上面所说的运算就是一个群,易证。
乘法逆元也是在一个群中的逆元。这个群是 。其中 ,也就是说在 中与 互质( 可以表示 的最大公约数)的正整数组成的集合。而 是指在 意义下的乘法。封闭性比较显然吧,从质因数的角度分析。
思考5:它为什么所有元素都有逆元?
证:首先转化问题为 ( 和 均为小于等于 的正整数)。就是说, 与 互质等价于存在一个 使得 在 的情况下和 相等,也就是和 相等。
算了这个太难打完了就不打了吧(
来源:《应用近世代数》清华大学出版社 胡冠章 王殿军 编著
应用以后再写(
数论函数简介及其性质与算法
积性函数
狄利克雷卷积
BSGS 及其拓展
注:此节内所有变量所表示的数均为非负整数。
基础 BSGS
基础 BSGS 算法可以在 或 的时间内, 的空间内解决已知 ,求最小非负整数 使 的问题。
设 。由原式两侧同乘 ,其中 为满足 且 的最大值,可得 。这个式子在 时与原式等价。推得 ,则 ,。由 得出如果存在 ,则一定存在 。
于是就可以先计算出 至 ,并将它们放入 Hash 表或 STL map 中,值为 的次方。注意如果 ,则表中这一项的值应为 。
之后再从 到 枚举 ,推出 ,并查表。如果查到,那么直接得出答案。如果枚举完还没查到,那么无解。
例题:[SDOI2011] 计算器、[SDOI2013] 随机数生成器。在这两题中,注意特判 。
模板代码
typedef long long ll;
ll BSGS(ll a, ll b, ll p) {
a %= p, b %= p;
if(a == 0) return b? -1: 1;//0^0 未定义, 0^1=0
map<ll, int> mp;
ll k = sqrt(p) + 1, c = 1; //k 再大些也无关紧要
for(int x = 1; x <= k; x++) {
c = c * a % p;
mp[c * b % p] = x;
}
ll d = c;
for(int q = 1; q <= k; q++) {
if(mp.find(d) != mp.end()) return k * q - mp[d];
d = d * c % p;
}
return -1;
}
拓展 1
显然,有时不会满足 的条件。OI-wiki 写的很好,就不写了吧(
拓展 2
有时又要求 。OI-wiki 写的也很好,也不写了吧(
例题:CodeForces 1106F(还用矩阵快速幂)
欧几里德算法及拓展欧几里德算法
大概这东西算是依铃学的最迷糊的比较简单的算法了……所以来补一补。(其实是做题遇到了发现就这算法不会(
欧几里德算法求
欧几里德算法求 就是由于 ( 不为 时)、,所以每次用较大的数模较小的数,至少减少一半,因此复杂度是 的。也叫辗转相除法。在 OI-Wiki 上有详细的讲解,就不说了吧(
扩展欧几里德算法求 整数解
而扩展欧几里德则使用了类似的辗转相除的方法来计算 的整数解。
设 。
求解过程在 OI-Wiki 上写的很好,所以就不写了吧(
拓展欧几里德的应用
有整数解的条件
将 变换为 ,显然 有整数解 ( 是整数)
当 ( 是整数)时,可通过以下方式求出解。
应用拓展欧几里得算法,求出 的一组解 。再让 ,于是求得一组整数解 。
因此 有整数解 ( 是整数)
求出 的所有整数解
首先将 都除掉 使处理后的 互质。
设 为方程 的一组解。
,所以,若 为方程 的一组解,则该方程的解集为 。
当使用拓展欧几里德(以及上面的结论)求出一组整数解 后,该方程的整数解集就是 。
FFT & NTT
原根
计算几何基础
树链剖分
轻重链剖分
树链剖分,就是说把树拆成链来方便处理、维护。轻重链剖分,就是按照“轻重”孩子节点来剖分。考虑这么一道题(Luogu P3384):
已知一棵包含 个结点的有根树,每个节点上包含一个数值,需要支持以下操作:1 x y z
,表示将树从 到 结点最短路径上所有节点的值都加上 。2 x y
,表示求树从 到 结点最短路径上所有节点的值之和。3 x z
,表示将以 为根节点的子树内所有节点值都加上 。4 x
,表示求以 为根节点的子树内所有节点值之和。
某位神仙曾经说过,图上问题不会做,先考虑树怎么做。树上问题不会做,先考虑链上怎么做。
发现当树是链时,就是区间加区间查询和,用线段树维护即可。考虑把树变成一些链,使每个链上节点在重新标号后是连续的。这样就可以把一条路径拆成一些区间,并对每个区间分别加减。
显然分出来的链上节点只能有祖先孩子关系,就是说只能直上直下,不能在某个位置有从向上变成向下的转弯。否则就不能保证编号的连续。
对于某一个节点,它可以连上某一个孩子 的链,而让其它孩子分别新建一个链。之后重编号时给自己编号后先给 的这颗子树编号,就能保证编号连续。显然, 的选择直接影响到路径拆分出来的区间数,也就影响到了复杂度。
那么, 应该如何选择呢?
一种方式是发挥随机的精神,在所有孩子中随机选择 。在数据随机时表现优秀,但是在极限数据下拆分出的区间个数是 的,如下图所示,从点 到点 的期望拆分出的区间个数约为 。
随机选择的 hack
还有一种方式,考虑贪心。显然我们想使路径拆分出来的区间数最小,所以 的子树最大会比较好。这就是重链剖分,可以证明拆分出的区间个数是 的。
(待补全)
长链剖分
长链剖分和轻重链剖分的用处差别比较大。
(待补全)
平衡树-Treap
平衡树分 leafy 和非 leafy。leafy 平衡树在叶子节点上存储原始信息,像线段树。非 leafy 则是每个节点上都有信息。实际上各种树型 之类的数据结构本质上都类似(比如平衡树、线段树、跳表),只是具体实现不同。
讲平衡树前,首先要讲一下排序二叉树(BST)是啥。排序二叉树,显然要是一个二叉树。每个节点上有一个 ,这棵树的中序遍历是不降的。也就是说,一个节点的左子树中所有的节点的 都小于等于这个节点的 ,而它的右子树中所有节点的 都大于这个节点的 。于是,可以在 的复杂度内完成许多操作,比如查询第 大的 、查询小于 的 的个数。
考虑如何做这些操作(均设当前节点为 ):
- 插入 :从根开始向下找,若 ,则递归至右孩子。否则递归至左孩子。当 为空时,新建节点。
- 找到第 大的 :维护前每个节点的子树大小。从根开始向下找,如果左孩子子树大小大于等于 时,递归至左孩子。如果左孩子子树大小等于 ,则返回当前节点的 。否则,将 减去左孩子子树大小再减去 (当前节点),并递归右孩子。
- 其它也类似,大概就是从根开始,根据一些信息来判断向左或向右递归,再去做些操作。
具体实现时,可以用指针表示左右孩子,可以用成员函数来写,也可以用下标表示,可以用普通函数写。建议用下标表示孩子,并用结构体封装节点,用引用来操作(即代码中的 node &p=tr[rt];
)。
可以做 Luogu P5076。
一部分 Code
struct node {
int val;
int siz; //本子树大小
int lc, rc; //左右孩子编号,0 为无孩子
} tr[N];
void upd(int rt) {
if(!rt) return;
node &p = tr[rt];
p.siz = tr[p.lc].siz + 1 + tr[p.rc].siz;
}
int find_val(int rt, int k){//找到第 k 大的 val
if(!rt) return -1;
node &p = tr[rt];
if(tr[p.lc].siz+1 > k) return find_val(p.lc, k);
else if(tr[p.lc].siz + 1 == k) return p.val;
else return find_val(p.rc, k - tr[p.lc].siz - 1);
}
int find_num(int rt, int k){//小于 k 的 val 的个数
if(!rt) return 0;
node &p = tr[rt];
if(p.val < k) return find_num(p.rc, k) + tr[p.lc].siz + 1;
else return find_num(p.lc, k);
}
int find_pre(int rt, int k){//小于 k 的最大值
if(!rt) return −2147483647;
node &p = tr[rt];
if(p.val < k) return max(p.val, find_pre(p.rs, k));
else return find_pre(p.ls, k);
}
int insert(int rt, int val){
if(!rt) return tr[++cnt] = node{val,1,0,0}, cnt;
node &p = tr[rt];
if(p.val < val) p.rc = insert(p.rc, val);
else p.lc = insert(p.lc, val);
upd(rt);
return rt;
}
其它的也都易于实现。但显然的,如果连续插入严格升序的 ,复杂度是 的。于是,需要降低树高。
平衡树是什么?平衡树就是比较平衡的 BST,每个节点左子树大小和右子树大小差不多(非严格定义)。设一个平衡树共 个节点,那显然树高是 级别的。那么如何把 BST 搞平衡呢?很简单,将每个节点再赋一个随机的 ,使得每个节点的父节点的 比它大,也就是说按 就是一个大根堆(小根堆也可以)。这样的树高期望就是 的了。作者不太会证明。
然后思考如何插入。
(以下为 Treap 的分裂合并式讲解)
此时,可以发现,这颗树可以分裂成两棵有序的树。可以按 分裂(小于 的在左,其余的在右),也可以按树的大小分裂。
Code
struct pir { int a, b; }; //a和b含义 把一个树裂成两个之后的树根编号,0 则为空
pir split_val(int rt, int val) {
if(!rt) return pir{0, 0};
node &p = tr[rt];
if(p.val < val) {
pir o = split_val(p.rs, val);
p.rs = o.a; o.a = rt; upd(rt);
return o;
} else {
pir o = split_val(p.ls, val);
p.ls = o.b; o.b = rt; upd(rt);
return o;
}
}
pir split_rk(int rt,int k) {
//如上易得。分裂后左树的大小为 k。
}
也可以发现,如果有两个有序的树(左边的树中最大值小于等于右边的最小值),可以把它们合并。由于树有序了,所以合并时只需要考虑 的大小。 较大的放在根,并将某一边的子树和另一个合并。
Code
int merge(int a, int b) {
if(!a || !b) return a + b;
node &pa = tr[a], &pb = tr[b];
if(pa.pri > pb.pri) {
pa.rs = merge(pa.rs, b);
upd(a); return a;
} else {
pb.ls = merge(a, pb.ls);
upd(b); return b;
}
}
如果要插入一个值,就直接将树按这个值裂开为两个,设较小的为 ,较大的为 ,再新建一个单独的节点表示所插入的值。之后将 和这个节点合并,再合并上 就行。
Code
int insert(int rt, int val){
pir o = split_val(rt, val);
tr[++cnt] = node{val, rand(), 1, 0, 0};// val, pri, siz, lc, rc
o.a = merge(o.a, cnt);
return merge(o.a, o.b);
}
其余的没什么变化。也可以将各个查询都按分裂,取出节点再合并的方式进行,但是常数会大些,也不很好写。
同时,由于可以分裂合并,Treap 可以维护(单个或多个)序列。 就是所谓的 。但是为了在中间插入,可以直接舍弃掉 而按照大小分裂。这时某个节点的排名就是 。序列的信息可以储存在每个节点中,由此也可以实现区间加,区间翻转,区间平移之类的操作。类似于线段树,都是打一个 tag,并下传。
区间翻转 Code
//需要一个tag表示是否翻转,同时 pd 函数应该在所有向下找的函数中使用,类似于线段树的 push_down
void pd(int rt){
if(!rt) return;
node &p = tr[rt];
if(!p.tg) return;
if(p.ls) {node &ls = tr[p.ls]; ls.tg ^= 1; swap(ls.ls, ls.rs); }
if(p.rs) {node &rs = tr[p.rs]; rs.tg ^= 1; swap(rs.ls, rs.rs); }
p.tg = 0;
}
void revers(int l, int r){
pir o = split_rk(rt,r),v = split_rk(o.a, l-1);
node &p = tr[v.b];
p.tg ^= 1; swap(p.ls, p.rs);
o.a = merge(o2.a,o2.b);
rt = merge(o.a,o.b);
}
你甚至可以使用 Treap 来维护环。它与维护序列类似,但需要分类讨论,断开哪里连接哪里,也需要维护一级祖先节点,用来按某个节点分裂。
放个维护序列的典型题:Luogu P2042 [NOI2005] 维护数列。希望你能在 1 天中写完并调完它。(附区间最大子段和维护方式(线段树):Luogu P4513。这东西在某次 CSP 初赛也出现过。)
其它例题:Luogu 模板题,[NOI2021] 密码箱。
催更催更![[傲娇]](https://blog.10circle.moe/usr/themes/G/static/img/loading2.gif)
你要看啥qwq(某一种算法或知识),抽时间更
树链剖分什么没学过的都行![[妙啊]](https://blog.10circle.moe/usr/themes/G/static/img/loading2.gif)