10circle 算法学习笔记

1137天前 · OI · 710次阅读

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!!

这个东西是用来解决一堆字符串共同执行某个操作的(比如查询在另一个字符串中出现的次数)。

首先对这一堆字符串建一个字典树。字典树上一个节点就是一个状态,可以表示从根到它的边所连起来形成的字符串,即某个(些)串的一个前缀。下文 $u$ 均表示某个状态。定义 $\texttt{fa}_ u$ 为 $u$ 在字典树上的父亲状态,$\texttt{to}_ {u,c}$ 为 $u$ 状态再接 $c$ 后的状态。

AC 自动机中最重要的就是 $\texttt{fail}$ 指针了。$\texttt{fail}_ u$ 就指向 $u$ 所表示的字符串在本字典树内的最长后缀的状态。感觉自己不会说话,如果不明白可以看 OI-wiki 中对 $\texttt{fail}$ 指针的说明。

构建 $\texttt{fail}$ 指针需要从短到长构建。

若 $\texttt{to}_ {\texttt{fail}_ u,c}$ 存在,则 $\texttt{fail}_ {\texttt{to}_ {u,c}}=\texttt{to}_ {\texttt{fail}_ u,c}$,即本字符串与最长后缀同时增加了字符 $c$,显然还是最长后缀。

其余情况,需要不断向前跳。由最长后缀跳到最长后缀的最长后缀……直到存在 $c$ 的边。这样可以从长到短遍历所有在字典树中的本字符串的后缀。如果始终没有 $c$ 的边,则 $\texttt{fail}_ u$ 就是空串。

但是这样不好写而且慢,需要优化。跳,可以路径压缩。因为不存在 $\texttt{to}_ {\texttt{fail}_ u,c}$,所以可以把它设成跳完的,即 $\texttt{to}_ {\texttt{fail}_ {\texttt{fail}_ u},c}$,相当于直接扯到存在 $c$ 的地方了。

查询就沿字典树游走。由于已经进行了路径压缩,不用判断到结尾要跳 $\texttt{fail}$。沿 $\texttt{fail}$ 累加某个状态出现了多少次。就是先枚举子串后端点,再沿 $\texttt{fail}$ 跳前端点。但是这样不好,慢。需要先标记一下,最后再累加。这样就是线性的了。

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 好像都可以只用坐标表示向量。向量记作 $\vec{a}$ 或 $\mathbf{a}$。

比如一个二维向量,就可以表示为 $(a,b)$ 的样子。就相当于变化量。当然向量也有多维的,于是 vector 就出现了。一个 $n$ 维的向量可以用 $(a_1,a_2,\cdots,a_n)$ 表示。用坐标理解这个东西可能好理解些。零向量没有大小,自然也不确定方向,相当于一个点,用 $\mathbf{0}$ 表示,其 $n$ 维坐标都是 $0$。

向量是可以相加的,变化量可以在这里体现。比如 $\mathbf{a+b}=(a_1+b_1,a_2+b_2,\cdots,a_n+b_n)$,就是每一维相加。相减类似,也是每一维相减,可以理解为 $\mathbf{a+(-b)}$。加的几何意义就是从原点开始按照 $\mathbf{a}$ 平移之后再按照 $\mathbf{b}$ 平移。比如一个点 $(x,y)$ 按二维向量 $(a_1,a_2)$ 平移后就会变成 $(x+a_1,y+a_2)$。这个东西可能有图更好理解些……但不好弄(。可以自己画几个图感受一下(,依铃不太会说(。不过这东西高中会学的。

向量还可以数乘,就是直接放大一些倍。$k\mathbf{a}=(ka_1,ka_2,\cdots,ka_n)$,然后方向就可能相反,也可能和原来一样。

两个向量共线就是说这两个线段平行(向量是可以任意平移的,就和线段一样,端点变了但是大小方向不变也是同一个向量),用数乘表示就是两个非零向量 $\mathbf{a}$ 与 $\mathbf{b}$ 共线 $\iff$ 有唯一实数 $k$,使得 $k\mathbf{a}=\mathbf{b}$。

向量的模就是向量的长度也就是向量的大小(明明是一个东西但它就是有一堆词),用 $|\mathbf{a}|$ 表示。$|\mathbf{a}|= \sqrt{a_1^2+a_2^2+\cdots+a_n^2}$。(这就是两点间距离公式,应该还好,会勾股定理就能推出来)。

矩阵及快速幂矩阵的设计

还有一个东西,叫矩阵。

$$\begin{bmatrix}a_{1,1}&\cdots&a_{1,m}\\\vdots&\ddots&\vdots\\a_{n,1}&\cdots&a_{n,m}\end{bmatrix}$$

这是一个 $n$ 行 $m$ 列的矩阵,你可以当它是一个二维数组。这里依铃用 $A_n$ 表示 $A$ 的行数,$A_m$ 表示 $A$ 的列数,$A_{i,j}$ 表示 $A$ 中第 $i$ 行第 $j$ 列的元素。

矩阵的加减要求 $A_n=B_n,A_m=B_m$,即运算的两个矩阵行数和列数相等,然后逐个元素进行。

矩阵的乘法要求 $A_m=B_n$,即第一个矩阵的列数等于第二个矩阵的行数。然后,设结果为 $C$,那么 $C_{i,j}=\sum_{k=1}^{A_m}A_{i,k}B_{k,j}$。显然 $C_n=A_n,C_m=B_m$。通俗的讲,在矩阵乘法中,$C$ 矩阵的第 $i$ 行第 $j$ 列的数,就是由 $A$ 第 $i$ 行的 $A_m$ 个数与 $B$ 第 $j$ 列的 $A_m$ 个数分别相乘再相加得到的。矩阵乘法没有交换律,但满足结合律。

接下来是用处,矩阵加速递推。有一个递推式,其中 $k_i$ 给定:

$$\large{f_i=k_1f_{i-2}+ k_2g_{i-1}+k_3f_{i-1},\ \ g_{i}=k_4g_{i-2}+k_5g_{i-1}+k_6f_{i-2}}$$

于是思考将它优化至 $\mathrm{O}(\log n)$,这需要构造一个初始矩阵 $A_2$ 和一个转移矩阵 $B$。$A_i$ 需要包含 $f_i,f_{i-1},g_{i},g_{i-1}$,可以写做 $$\begin{bmatrix}f_i&f_{i-1}&g_i&g_{i-1}\end{bmatrix}$$。而 $B$ 需要将 $A_{i-1}$ 转移到 $A_i$,可以写做:

$$\begin{bmatrix}k_3&1&0&0\\k_1&0&k_6&0\\k_2&0&k_5&1\\0&0&k_4&0\end{bmatrix}$$

然后答案矩阵为 $A_2B^{n-2}$。它可以写作 $\begin{bmatrix}f_n&f_{n-1}&g_n&g_{n-1}\end{bmatrix}$。注意可以优化的递推式只有常系数(好像是的)。快速构造的方法就是

$$\begin{bmatrix}&f_i&f_{i-1}&g_i&g_{i-1}\\f_{i-1}&&&&\\f_{i-2}&&&&\\g_{i-1}&&&&\\g_{i-2}&&&&\end{bmatrix}$$

然后往里面填数就行。左侧就是矩阵乘法中那个 $A_{i-1}$,而转移后就是上面的一行,也就是要求的 $A_i$。然后快速幂就行。

矩阵也可以表示变换,方式同上,不细说了。

例题: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;
}

分数规划

高斯消元

中国剩余定理

拉格朗日插值

大纲里没有但是它考了。离谱。

莫比乌斯反演应用

数论分块

群及其应用

进行运算的东西和结果的类型相同(类型的概念很模糊,只是为了感性理解。如果是群或类似概念,就是在一个集合中)的运算是有封闭性的。比如 $<$ 在实数范围内就是没有封闭性的。

为了更好的探究运算的性质,数学家们引入了。群由一个集合和一个集合上的运算构成。如果二元运算 $\cdot$(泛指运算)在集合 $G$ 中有封闭性和结合律(注意并不用交换律),那么它们就构成了半群 $(G,\cdot)$。注意这还不是群,只是半群。

群还需要单位元。单位元一般用 $e$ 表示。它的定义是 $\forall a \in G, e\cdot a= a\cdot e = a$。$\forall$ 的意思就如同它的 LaTeX 名一般,是“对于所有”的意思。$\in$ 的意思也如同它的 LaTeX 名一般,是表示一个元素属于一个集合的意思。为啥叫单位元呢,首先这是个元素,而且它就像是单位 $1$ 一样,任何数和它做运算都不会改变。含有单位元的半群叫做含幺半群

思考 1:会不会存在两个单位元呢?可以试着写出证明。tip:$e_1 \cdot e_2=?$。既是 $e_1$ 又是 $e_2$,所以只有一个单位元。

思考 2:若 $\forall a \in G, e_l\cdot a = a$,$\forall a \in G, a\cdot e_r = a$,那么 $e_l$ 和 $e_r$ 是不是相同呢?

证:$$\because \forall a \in G,e_l \cdot a = a\ \ \ \ \therefore e_l\cdot e_r=e_r$$ (第一步,就是说把 $a$ 替换为 $e_r$ )

$$\because \forall a \in G,a\cdot e_r = a,\ \ \ \ \therefore e_l\cdot e_r=e_l$$ (第二步)

$$ \therefore e_l\cdot e_r=e_l=e_r$$ (第三步)(非常简单是不是(

也许需要举个例子……?比如 $(\Z,+)$ 就是一个含幺半群(也是一个群),其中 $\Z$ 指整数集。对,单位元 $e=0$。但是 $(\Z,\times)$ 是含幺半群却不是群,因为逆元(下面讲)可能不是整数,不属于整数集合 $\Z$。

然而含幺半群还不是群,群中的每一个元素还有它的逆元。$a$ 的逆元一般用 $a^{-1}$ 表示。同时,在下面的叙述中,$a^{-1}$ 都指逆元。逆元的定义是 $a^{-1}\cdot a = a\cdot a^{-1}=e$。注意,下面两个都是在含幺半群的条件下的证明,不是所有元素都有逆元。

思考 3:逆元可能有两个吗?

思考 4:若 $a^{-1}_l \cdot a =e, a\cdot a^{-1}_r=e$,那么 $a^{-1}_l$ 和 $a^{-1}_r$ 是否相同?(显然下面这个问题严格强于上面的,因此只证下面的了。

证:$\because a^{-1}_l \cdot a =e,\ \ \ \ \therefore a^{-1}_l \cdot a \cdot a^{-1}_r=e\cdot a^{-1}_r=a^{-1}_r$。

$$\therefore a^{-1}_l \cdot (a \cdot a^{-1}_r)=a^{-1}_r,\ \ \ \ \therefore a^{-1}_l \cdot e=a^{-1}_r,\ \ \ \ \therefore a^{-1}_l=a^{-1}_r $$

群要求 $\forall a \in G, \exist a^{-1},\text{ s.t. }a^{-1}\cdot a = a\cdot a^{-1}=e$。$\exist$ 的意思就和它的 LaTeX 名一样,是“存在一个”的意思。$\text{s.t.}$ 全称 subject to, 意思是使得…满足…。于是群的定义就讲完了。

群中一个元素和它自己做 $k$ 次运算,即 $$\begin{matrix}\underbrace{a\cdot a \cdots a}\\k\end{matrix}$$,表示为 $a^k$。看着和幂很像,也可以以幂理解,但不一样。那么,很显然的,这个有整数幂的性质,包括负数。

比如一个长度为 $n$ 的排列(置换)和上面所说的运算就是一个群,易证。

乘法逆元也是在一个群中的逆元。这个群是 $(\Z^{*}_{n},\cdot)$。其中 $\Z^{*}_{n}=\{i|1\le i \le n,(n,i)=1\}$,也就是说在 $[1,n]$ 中与 $n$ 互质($(a,b)$ 可以表示 $a,b$ 的最大公约数)的正整数组成的集合。而 $\cdot$ 是指在 $\bmod n$ 意义下的乘法。封闭性比较显然吧,从质因数的角度分析。

思考5:它为什么所有元素都有逆元?

证:首先转化问题为 $(a,n)=1\Leftrightarrow \exist x \text{ s.t. } a\cdot x \equiv1\pmod{n}$($a$ 和 $x$ 均为小于等于 $n$ 的正整数)。就是说,$a$ 与 $n$ 互质等价于存在一个 $x$ 使得 $a\cdot x$ 在 $\bmod n$ 的情况下和 $1$ 相等,也就是和 $e$ 相等。

算了这个太难打完了就不打了吧(

来源:《应用近世代数》清华大学出版社 胡冠章 王殿军 编著

应用以后再写(

数论函数简介及其性质与算法

积性函数

狄利克雷卷积

BSGS 及其拓展

注:此节内所有变量所表示的数均为非负整数。

基础 BSGS

基础 BSGS 算法可以在 $O(\sqrt{p})$ 或 $O(\sqrt{p}\log{p})$ 的时间内,$O(\sqrt{p})$ 的空间内解决已知 $a,b,p\ (\gcd(a,p)=1)$,求最小非负整数 $x$ 使 $a^x\equiv b \pmod p$ 的问题。

设 $k=\lceil\sqrt{p}\rceil$。由原式两侧同乘 $a^r$,其中 $r$ 为满足 $1\le r \le k$ 且 $k|(x+r)$ 的最大值,可得 $a^{x+r}\equiv a^{r}b \pmod p$。这个式子在 $(a,p)=1$ 时与原式等价。推得 $a^{qk}\equiv a^{r}b \pmod p$,则 $x=qk-r$,$1\le q\le k$。由 $a^{\phi(p)}\equiv 1 \pmod p$ 得出如果存在 $x$,则一定存在 $r,q$。

于是就可以先计算出 $ab$ 至 $a^kb$,并将它们放入 Hash 表或 STL map 中,值为 $a$ 的次方。注意如果 $a^db=a^eb\ (d<e)$,则表中这一项的值应为 $e$。

之后再从 $1$ 到 $k$ 枚举 $q$,推出 $a^{qk}$,并查表。如果查到,那么直接得出答案。如果枚举完还没查到,那么无解。

例题:[SDOI2011] 计算器、[SDOI2013] 随机数生成器。在这两题中,注意特判 $a=0$。

模板代码

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

显然,有时不会满足 $(a,p)=1$ 的条件。OI-wiki 写的很好,就不写了吧(

拓展 2

有时又要求 $x^a\equiv b\pmod p$。OI-wiki 写的也很好,也不写了吧(

例题:CodeForces 1106F(还用矩阵快速幂)

欧几里德算法及拓展欧几里德算法

大概这东西算是依铃学的最迷糊的比较简单的算法了……所以来补一补。(其实是做题遇到了发现就这算法不会(

欧几里德算法求 $\gcd$

欧几里德算法求 $\gcd$ 就是由于 $\gcd(a,b)=\gcd(a,a%b)$ ($b$ 不为 $0$ 时)、$\gcd(a,0)=a$,所以每次用较大的数模较小的数,至少减少一半,因此复杂度是 $\log$ 的。也叫辗转相除法。在 OI-Wiki 上有详细的讲解,就不说了吧(

扩展欧几里德算法求 $ax+by=\gcd(a,b)$ 整数解

而扩展欧几里德则使用了类似的辗转相除的方法来计算 $ax+by=\gcd(a,b)$ 的整数解。
设 $g=\gcd(a,b)$。

求解过程在 OI-Wiki 上写的很好,所以就不写了吧(

拓展欧几里德的应用

$ax+by=c$ 有整数解的条件

将 $ax+by=c$ 变换为 $g(\frac{a}{g}x+\frac{b}{g}y)=c$,显然 $ax+by=c$ 有整数解 $\Rightarrow$ $c=kg$($k$ 是整数)

当 $c=kg$($k$ 是整数)时,可通过以下方式求出解。
应用拓展欧几里得算法,求出 $ax+by=\gcd(a,b)$ 的一组解 $x^\prime,y^\prime$。再让 $x_0=kx^\prime,y_0=ky^\prime$,于是求得一组整数解 $x_0,y_0$。

因此 $ax+by=c$ 有整数解 $\iff$ $c=kg$($k$ 是整数)

求出 $ax+by=c$ 的所有整数解

首先将 $a,b,c$ 都除掉 $g$ 使处理后的 $a,b$ 互质。

设 $x_0,y_0$ 为方程 $ax+by=c$ 的一组解。
$ax+by=a(x_0+bt)+b(y_0-at)=ax_0+abt+by_0-abt=ax_0+by_0=c$,所以,若 $x_0,y_0$ 为方程 $ax+by=c$ 的一组解,则该方程的解集为 $\{x,y|x=x_0+bt,y=y_0-at,t\in \R\}$。
当使用拓展欧几里德(以及上面的结论)求出一组整数解 $x_0,y_0$ 后,该方程的整数解集就是 $\{x,y|x=x_0+bt,y=y_0-at,t\in \Z\}$。

FFT & NTT

原根

计算几何基础

树链剖分

轻重链剖分

树链剖分,就是说把树拆成链来方便处理、维护。轻重链剖分,就是按照“轻重”孩子节点来剖分。考虑这么一道题(Luogu P3384):

已知一棵包含 $n$ 个结点的有根树,每个节点上包含一个数值,需要支持以下操作:
1 x y z,表示将树从 $x$ 到 $y$ 结点最短路径上所有节点的值都加上 $z$。
2 x y,表示求树从 $x$ 到 $y$ 结点最短路径上所有节点的值之和。
3 x z,表示将以 $x$ 为根节点的子树内所有节点值都加上 $z$。
4 x,表示求以 $x$ 为根节点的子树内所有节点值之和。

某位神仙曾经说过,图上问题不会做,先考虑树怎么做。树上问题不会做,先考虑链上怎么做。
发现当树是链时,就是区间加区间查询和,用线段树维护即可。考虑把树变成一些链,使每个链上节点在重新标号后是连续的。这样就可以把一条路径拆成一些区间,并对每个区间分别加减。
显然分出来的链上节点只能有祖先孩子关系,就是说只能直上直下,不能在某个位置有从向上变成向下的转弯。否则就不能保证编号的连续。
对于某一个节点,它可以连上某一个孩子 $c_i$ 的链,而让其它孩子分别新建一个链。之后重编号时给自己编号后先给 $c_i$ 的这颗子树编号,就能保证编号连续。显然,$c_i$ 的选择直接影响到路径拆分出来的区间数,也就影响到了复杂度。

那么,$c_i$ 应该如何选择呢?
一种方式是发挥随机的精神,在所有孩子中随机选择 $c_i$。在数据随机时表现优秀,但是在极限数据下拆分出的区间个数是 $O(n)$ 的,如下图所示,从点 $1$ 到点 $n-1$ 的期望拆分出的区间个数约为 $\frac{n}{4}$。

随机选择的 hack

还有一种方式,考虑贪心。显然我们想使路径拆分出来的区间数最小,所以 $c_i$ 的子树最大会比较好。这就是重链剖分,可以证明拆分出的区间个数是 $O(\log n)$ 的。

(待补全)

长链剖分

长链剖分和轻重链剖分的用处差别比较大。

(待补全)

平衡树-Treap

平衡树分 leafy 和非 leafy。leafy 平衡树在叶子节点上存储原始信息,像线段树。非 leafy 则是每个节点上都有信息。实际上各种树型 $\log$ 之类的数据结构本质上都类似(比如平衡树、线段树、跳表),只是具体实现不同。

讲平衡树前,首先要讲一下排序二叉树(BST)是啥。排序二叉树,显然要是一个二叉树。每个节点上有一个 $val$,这棵树的中序遍历是不降的。也就是说,一个节点的左子树中所有的节点的 $val$ 都小于等于这个节点的 $val$,而它的右子树中所有节点的 $val$ 都大于这个节点的 $val$。于是,可以在 ${O}(\texttt{树高})$ 的复杂度内完成许多操作,比如查询第 $k$ 大的 $val$、查询小于 $k$ 的 $val$ 的个数。

考虑如何做这些操作(均设当前节点为 $p$):

  • 插入 $val$:从根开始向下找,若 $p_{val}<val$,则递归至右孩子。否则递归至左孩子。当 $p$ 为空时,新建节点。
  • 找到第 $k$ 大的 $val$:维护前每个节点的子树大小。从根开始向下找,如果左孩子子树大小大于等于 $k$ 时,递归至左孩子。如果左孩子子树大小等于 $k-1$,则返回当前节点的 $val$。否则,将 $k$ 减去左孩子子树大小再减去 $1$(当前节点),并递归右孩子。
  • 其它也类似,大概就是从根开始,根据一些信息来判断向左或向右递归,再去做些操作。

具体实现时,可以用指针表示左右孩子,可以用成员函数来写,也可以用下标表示,可以用普通函数写。建议用下标表示孩子,并用结构体封装节点,用引用来操作(即代码中的 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;
}

其它的也都易于实现。但显然的,如果连续插入严格升序的 $val$,复杂度是 $O(n^2)$ 的。于是,需要降低树高。
平衡树是什么?平衡树就是比较平衡的 BST,每个节点左子树大小和右子树大小差不多(非严格定义)。设一个平衡树共 $n$ 个节点,那显然树高是 $\mathrm{O}(\log n)$ 级别的。那么如何把 BST 搞平衡呢?很简单,将每个节点再赋一个随机的 $pri$,使得每个节点的父节点的 $pri$ 比它大,也就是说按 $pri$ 就是一个大根堆(小根堆也可以)。这样的树高期望就是 $O(\log n)$ 的了。作者不太会证明。

然后思考如何插入。
(以下为 Treap 的分裂合并式讲解)
此时,可以发现,这颗树可以分裂成两棵有序的树。可以按 $val$ 分裂(小于 $val$ 的在左,其余的在右),也可以按树的大小分裂。

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。
}

也可以发现,如果有两个有序的树(左边的树中最大值小于等于右边的最小值),可以把它们合并。由于树有序了,所以合并时只需要考虑 $pri$ 的大小。$pri$ 较大的放在根,并将某一边的子树和另一个合并。

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;
    }
}

如果要插入一个值,就直接将树按这个值裂开为两个,设较小的为 $a$,较大的为 $b$,再新建一个单独的节点表示所插入的值。之后将 $a$ 和这个节点合并,再合并上 $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 可以维护(单个或多个)序列。$val$ 就是所谓的 $i$。但是为了在中间插入,可以直接舍弃掉 $val$ 而按照大小分裂。这时某个节点的排名就是 $i$。序列的信息可以储存在每个节点中,由此也可以实现区间加,区间翻转,区间平移之类的操作。类似于线段树,都是打一个 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] 密码箱。

左偏树

可持久化

Euler Tour Tree

LCT(大概率不会学)

👍 4

none

最后修改于556天前

评论

取消回复
贴吧 狗头 原神 小黄脸
收起

贴吧

  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡

狗头

  • 狗头
  • 狗头
  • 狗头
  • 狗头
  • 狗头
  • 狗头
  • 狗头
  • 狗头
  • 狗头
  • 狗头
  • 狗头
  • 狗头

原神

  • 原神
  • 原神
  • 原神
  • 原神
  • 原神
  • 原神
  • 原神
  • 原神
  • 原神
  • 原神
  • 原神
  • 原神
  • 原神
  • 原神
  • 原神
  • 原神
  • 原神
  • 原神
  • 原神
  • 原神
  • 原神
  • 原神
  • 原神

小黄脸

  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  1. ice641 1115天前

    催更催更 [傲娇]

    1. 10circle 1113天前

      你要看啥qwq(某一种算法或知识),抽时间更

      1. ice641 1113天前

        树链剖分什么没学过的都行 [妙啊]

目录

avatar

10circle

OIer,qwq

28

文章数

22

评论数

6

分类