基础数据结构学习笔记
Vanilla_chan
·
·
个人记录
基础数据结构学习笔记
来自\color{Gray}\texttt{SharpnessV}的省选复习计划中的基础数据结构。
应某\color{gray}sto毒瘤\color{gray}orz的要求,笔记顺序按照题单中的题目顺序编排。
基础数据结构,顾名思义,就是比较简单的数据结构了。
先列出会用到的基础数据结构/算法/思想吧:
- 并查集
1,3
- 队列
2
- 线段树
3,7,12,13
- 分块
3,9
- 前缀和
4
- ST表
4
- 二叉堆
4
- 树状数组
5,6
- 根号分治
8,9
- 平衡树
10,11
- 树链剖分
12,13
下面开始讲例题。
例题1
P1955 [NOI2015] 程序自动分析
有一堆类似于x_i=x_j和x_i\neq x_j的n(n\le10^6)个条件,询问这些条件能否同时满足。
并查集可以维护x_i=x_j但是不能维护x_i\neq x_j,因为不等于不具有传递性。
考虑离线,然后对于所有的x_i=x_j进行merge(i,j);再对于所有的x_i\neq x+j查询他们是否不在一个集合内。
注意i,j\le10^9,所以要离散化。
Code
例题2
P2827 [NOIP2016 提高组] 蚯蚓
\color{gray}这道题的题面和解题过程都有点长,所以我就直接帮IVfixed吧
一共有n只蚯蚓,每只蚯蚓有一个长度,并且蚯蚓会按每年q厘米的速度增长。现在每年选择一只最长的蚯蚓,将其按p的比例切成两半,被切的的蚯蚓这一年不能增长,问每年被切的蚯蚓的长度和m年后所有蚯蚓的长度(据题意只需输出部分结果)
看到这题首先想到的是使用堆来做(因为,切蚯蚓符合堆的查询极值、插入和删除)。但考虑到数据的范围(1\le n\le10^5,0\le m\le 7\times 10^6),由于使用堆的时间复杂度为\rm O(m\times \log n),其结果必然会导致超时,所以只有另辟蹊径。
再仔细观察,发现每年切最长的一只蚯蚓,说明切过的蚯蚓长度具有单调性,即先切的蚯蚓的左段一定大于等于后切蚯蚓的左段,先切蚯蚓的右段一定大于等于后切蚯蚓的右段。
\color{white}喷IV滥用标题行awa
以下为证明过程:为方便,设两只蚯蚓长度为a,b,切后的长度分别是a_1,a_2,b_1,b_2.
\because a\ge b\\
\therefore a\times p\ge b\times p,a-a\times p\ge b-b\times p\\
\because切这两只蚯蚓的时候这两只蚯蚓都少增长了q厘米\\
\therefore a_1\ge b_1,a_2\ge b_2
因此我们可以想到这题可以使用普通队列来做,使复杂度降至\rm O(m)。
使用3个队列分别保存没被切过的蚯蚓,被切过的蚯蚓左段,和被切过的蚯蚓右段。
每次取出3个队列中最长的一只蚯蚓,将其切断都分别放入后两个数组,依此类推循环m次即可求出结果。
还需要考虑蚯蚓的增长长度问题,由于每次除了被切的蚯蚓,其他所有的蚯蚓都在增长,所以只需要再从队列中取元素的时候加上(i-1)\times q,放回时减去i\times q的长度(因为被切的蚯蚓这年不增长,所以多减q厘米,具体实现参见程序)。
Code
例题3
P5012 水の数列
给你一个长度为n(n\le10^6)的数列,有T(T\le 10^3)组询问,每一组询问查询区间[l,r],请选择一个x,将所有\le x的数标记后得到的段数\in[l,r],得分为每一段连续标记的区间的长度的平方和除以x,最大化得分。
强制在线。
注意到没有修改,那么就可以愉快的预处理。
首先从小到大枚举$x(1\le x\le 10^6)$同时用**并查集**维护连通性,计算答案为$x$时有多少段,得分是多少。
把这个$(得分,x)$二元组放到一个数组中(下标为段数),用**线段树**维护即可。
结果大概可能是像我一样辛辛苦苦打完线段树,发现线段树无论如何都会$MLE$……线段树的空间复杂度是$4\times n$左右,而分块的空间复杂度只有$n+\sqrt{n}$,于是换成**分块**就可以愉快的AC啦!这是一种时间换空间的方法。
[Code]()
## 例题4
[P2048 [NOI2010] 超级钢琴](https://www.luogu.com.cn/problem/P2048)
首先一段长度为$n$的序列当然有$\rm O(n^2)$级别个子串,暴力当然是把这个写子串的得分全部计算出来,排序或者用堆并取前$k$个。
考虑优化:先做**前缀和**$sum_i$,方便差分表达一段区间的和。我们固定左端点为$s$,那么右端点的取值范围为$[s+L-1,min(s+R-1,n)]$(下文为了方便写作$[l,r]$)。假设在固定$s$的情况下,我们已经知道了有个$mid(mid\in[l,r])$可以使得$\forall x\in[l,r],sum_x-sum_{s-1}\le sum_{mid}-sum_{s-1}$,即若$mid$是对于三元组$(s,l,r)$的最优右端点,那么$\forall x\in [l,mid-1]\cup[mid+1,r]$都没有$mid$更优。(这不是废话嘛)
那么我们可以先把$\forall s\in[1,n-L+1]$三元组$(s,s+L-1,min(s+R-1,n))$放入堆中,并且用**ST表**计算使得答案最优的$mid$,那么这个三元组的得分就为$sum_{mid}-sum_{s-1}$。以此为优先级重载运算符即可。
每次从堆中取出一个三元组并加入答案之后,再将分裂的左右区间$[l,mid-1]$,$[mid+1,r]$加入堆中即可。(若区间为空就不加入!)
注意$ans$是 $10^4nk$ 级别的,需要使用$\text{long long}$。
[Code](https://www.luogu.com.cn/paste/7c6vf7nv)
## 例题5
[P1966 [NOIP2013 提高组] 火柴排队](https://www.luogu.com.cn/problem/P1966)
发现原式$\sum(a_i-b_i)^2=\sum(a_i^2+b_i^2-2a_ib_i)$,距离的变化只和$a_i\times b_i$有关,要使得$a_i\times b_i$越大越好。
这里要借助排序不等式,就可以知道要使得两个序列的每一对在同一个位置的数,他们在格子序列中的排名要相同。
首先把两个序列离散化至$[1,n]$,然后对于$a$记录$pos[i]$表示排名为$i$的数现在的位置。
然后对于$b$统计$pos[b[i]]$的逆序对即可。
这里可以用**树状数组**或者**归并排序**来计算逆序对。
[Code](https://www.luogu.com.cn/paste/a1a9fcgg)
## 例题6
[P4113 [HEOI2012]采花](https://www.luogu.com.cn/problem/P4113)
求区间出现次数$\ge 2$的数的个数。
$n\le 3\times 10^5
显然这不就是莫队直接过嘛
n\le 2\times 10^6
这还不就是莫队,卡卡常再来个指令集
注意到选择的区间中每个数第二次出现才会有$1$的贡献,那么我们不妨就把$1$放在每个数第二次出现的地方。
类似于莫队的思想,但是这一次我们只按照左端点 $l$ 来排序,方便统计贡献。
预先处理好每个数的后继(这里指的是下一次出现该数的位置),然后 $l$ 指针每移动一位,就处理好贡献转移:
- $l$这一位走出了区间,那么我们应当给$nxt_l$的贡献$-1$(因为他已经不是区间中第二次出现的数了,变成第一次出现的了)
- 既然$nxt_l$变成了$a[l]$第一次出现在该区间中的位置,那么相应的,$nxt_{nxt_l}$就会变成区间中第二次出现的数,获得贡献$+1
初始化是就先将[1,n]中第二次出现的数的贡献+1。
最后问的是[l,r],那么就用树状数组维护前缀和的贡献,差分出区间的贡献即可。
Code
例题7
P4198 楼房重建
在一个平面内,询问所有的高度大于0的点与(0,0)的连线没有与之前的线段相交的楼房.带修。
很好的线段树题目。
这道题告诉了我,线段树不是只是用来做带修区间和的。
既然是共用了原点(0,0),所以会影响答案的量就是线段的斜率。
如果说用线段树的话,大部分地方都非常显然,需要维护区间内有多少个楼房是可见的sum,区间最高的斜率k……但是唯独不知道如何合并两个区间。
首先,在左边的那一个区间的sum是一定可以直接被看到的,可以直接加入父区间的sum。
而右边的楼房中,只有斜率\ge 左区间的k的楼房才能被看到。为了计算右区间的贡献,我们用一个函数\rm calc(k,p)表示计算当左边有个线段的斜率是k时,线段树上的节点p所代表的区间中有多少楼房可以被看到(有贡献)。
在\rm calc(k,p)中我们继续递归计算。
- 若l_p=r_p,则只要k<k_p则
return 1,否则return 0.
- 将区间分成两部分,设为L,R:
- 若\rm k< k_L,那么R中的贡献是保持不变的,只需要再递归计算L的贡献。
- 若\rm k\ge k_L,那么左区间的贡献为0,只需要继续计算R的贡献。
需要注意的是,对于情况1,R的贡献不是R_{sum},而是Fa_{sum}-L_{sum}。因为R_{sum}的定义是R这个区间(在这个区间之前没有任何的阻碍物的情况下)有多少房屋能够被看到。
int calc(double k,int p)
{
if(l(p)==r(p)) return k<k(p);
int mid=l(p)+r(p)>>1;
if(k<k(p<<1)) return calc(k,p<<1)+sum(p)-sum(p<<1);
return calc(k,p<<1|1);
}
Code
例题8
P3396 哈希冲突
先想两种极端的做法
- 对于每个询问,暴力计算k,k+p,k+2\times p\dots的value之和。
- 预处理出ans[p][k]表示在\bmod p的意义下,余数为k的value之和。
第一种方法时间\rm O(n^2),空间\rm O(1);第二种方法时间\rm O(1),空间O(n^2)。
考虑根号分治。
对于p\le \sqrt n,我们预处理,保存在数组ans[p][k]中。空间\rm O(\sqrt n\times\sqrt n)=\rm O(n),时间上预处理\rm O(n\sqrt n),修改\rm O(\sqrt n),查询\rm O(1).
对于p>\sqrt n,我们不预处理,每一次询问暴力统计。每次统计的数量不会超过\dfrac np\le \sqrt n.
\color{white}我做了那些Ynoi我都白做了啊啊啊,这么简单的一个根号分治我都想不出来……我太菜了……\color{gray}awa
Code
例题9
区间众数。
普通版
前置知识:基本的分块,求众数。
本文将以从洛谷 P4168 [Violet]蒲公英的转换视角来解决这道题。蒲公英这道题不仅数据范围友好,题目背景也不错。
[P4168 [Violet]蒲公英](P4168 [Violet]蒲公英)
不妨可以去看看我的题解,顺便点个赞awa
给你一个长为 n 的序列 a,m 次询问,每次查询一个区间的众数的出现次数,强制在线。
1≤n,m≤5×10^5,0 \leq a_i\leq 10^9,0≤a_i≤10^9。
以下默认块大小为\sqrt n.
首先离散化。
预处理出从第i块到第j块中的众数zs_{i,j}。可以枚举i,枚举j,再枚举块内元素开一个桶统计。时间复杂度O(n\sqrt{n}),空间复杂度O(n)+O(\sqrt n\times \sqrt n)=O(n)。
预处理出第1块到第i块这个区间[1,i]中数字x出现的次数cnt_{i,x}。可以枚举i后,先将(\forall x)cnt_{i-1,x}拷贝到cnt_{i,x},再统计当前块i。时间复杂度O(\sqrt n\times (n+\sqrt n))=O(n\sqrt n),空间复杂度O(n\sqrt n)。
对于每一个询问(l,r),讨论其是否在一个块内。若是同块,就使用桶暴力统计。若不在一个块内,则将其拆为两个散块和一个整块。
首先答案ans=zs_{i,j},再向两边拓展,看看散块内的数x是否可能成为比ans出现次数更大的数。具体的,整块中x出现的次数可以用之前预处理出的cnt_{j,x}-cnt_{i-1,x}这种形式表示。散块中的x使用桶来统计即可。
询问的时间复杂度为O(\sqrt n+2\times \sqrt n)=O(\sqrt n)。
Code
加强版
蒲公英的数据范围十分友好:n\le 4\times 10^4,m\le 10^5。所以我们完全能够开下cnt_{s,n}。
但是P5048 [Ynoi2019 模拟赛] Yuno loves sqrt technology III的数据范围就不那么友好了:n,m\le 5\times 10^5。
这两道题有许多的共同点:区间众数,无修改,要离散化,数据范围看上去是根号做法的。
但是也有一些不同点:无法预处理前缀块的桶(因为空间开不下),求的是众数出现的次数而非众数是谁(其实差不多,但是求出现次数的话需要处理的信息可以少了点)
无法开桶,那么可以用另一种方式表达这个桶吗?可以的。
首先离散化,预处理zs_{i,j}就不必多费阐述了吧。
使用n个vector中存放下标,vector_{x,i}表示数x第i次出现的下标。由于是vector,所以总空间依然是O(n)的。
再记p_i表示i这位上的数x在其vector_{x}中在第几位。
显然预处理这个的时间复杂度是O(n)的。
对于每个询问,我们还是类似于上面的那样,同块暴力统计即可;
否则依然将其拆分。首先ans=zs_{i,j}(注意,这里的zs_{i,j}不再是[i,j]块内的众数,而是其出现的次数),在考虑两边的散块中,数x的出现次数能否更新ans。
比如,现在正在考虑在左侧散块的下标为i的数x能否更新ans。p_i是i之前有多少个x,那么至少要存在第ans+p_i个x,且ans+p_i要在[l,r]的数据范围以内,答案就可以被更新,ans++。
若在右侧则同理。
Code
小节
分块模板题的训练就到此完结了。
但是分块的玄学美妙优雅暴力之处不仅仅于此。
比如根号平衡等。
比如P5048 [Ynoi2019 模拟赛] Yuno loves sqrt technology III这道题其实还可以二分查找最后一个\le r的数,使得总时间复杂度变为O(NT+MN/T*\log t)\stackrel{T=\sqrt{N\log N}}{\longrightarrow}O(N\sqrt{N\log N})[^1]级别的。
大概是因为这只是Ynoi的模拟赛吧,就没有那么卡时间。
但是遇到毒瘤题,根号平衡是一种很重要的卡常方法。
以后遇到了卡常分块的话可以记录一下。
[^1]: 算法竞赛进阶指南 0x44
例题10
P2042 [NOI2005] 维护数列
请写一个程序,要求维护一个数列,支持以下6种操作:
- 在第pos个数字后面插入tot个数字
- 从第pos个数字开始删除tot个数字
- 从第pos个数字开始覆盖tot个数字为c
- 从第pos个数字开始的tot个数字翻转
- 区间和
- 最大子段和
显然用**平衡树**。这里使用$FHQ-Treap$。
- 插入一堆数字,先对这一堆数字进行$\rm O(n)$的建树,然后`split+merge`。
- 删除一堆数字,`split`后`delete`。
- 区间覆盖,`split`后打标记。
- 区间翻转,`split`后打标记。
- 区间和,平衡树基本操作,维护$sum$。
- 最大子段和,平衡树基本操作$lmax,rmax,zmax$。
所以这道题就是经典的平衡树$\color{white}SB$码农题了,使用$FHQ-Treap$或$Splay$即可。
如果用数组的话需要写垃圾回收,指针版就没有这么多麻烦,直接`delete`就好。
[Code](写这道题不如写下面这一道呢)
## 例题11$\color{gray}(报复社会)
P5066 [Ynoi2014] 人人本着正义之名
对一个01序列进行以下m个操作:
- 区间覆盖为
0
- 区间覆盖为
1
- 将区间[l,r-1]中的数a_i同时变为a_i与a_{i+1}按位或的值
- 将区间[l+1,r]中的数a_i同时变为a_i与a_{i-1}按位或的值
- 将区间[l,r-1]中的数a_i同时变为a_i与a_{i+1}按位与的值
- 将区间[l+1,r]中的数a_i同时变为a_i与a_{i-1}按位与的值
- 查询区间和
强制在线。
手写珂朵莉树模板题,建议只要会珂朵莉树(哪怕没有写过珂朵莉树的都行),写过非旋$Treap$的都可以来试一试。
思路很简单的,代码也不短,可以练练手吧。如果实在是不会的话就给我**点个赞**然后看看[题解](https://www.luogu.com.cn/blog/Vanilla-chan/luo-gu-p5066-ynoi2014-ren-ren-ben-zhuo-zheng-yi-zhi-ming)吧
按照传统(?),只给部分代码供参考。有问题也可以私信(如果我还没AFO的话)
[Code](https://www.luogu.com.cn/paste/mr5x0v35)
唉,2021年了,这道题需要一点卡常了……
原本$Ynoi$是很友好的,但是有些人用错误的时间复杂度乱搞过去了,所以只能加强加强再加强,导致正解想要过去也比较困难了……
[$\textbf{2021-03-31 08:00:25 thus,AC.}$](https://www.luogu.com.cn/record/48728244)
附上[debug日志](https://www.luogu.com.cn/blog/Vanilla-chan/luo-gu-p5066-ynoi2014-ren-ren-ben-zhuo-zheng-yi-zhi-ming-debug-log)。祝您早日AC。
## 例题12
[P3313 [SDOI2014]旅行](https://www.luogu.com.cn/problem/P3313)
树上每个点都有其颜色和权值。单点修改一个点的颜色或价值,查询树上一条链间颜色与起点/终点相同的点的权值之和/最大值。
对原树进行**树剖**,对每一种颜色建立**动态开点线段树**。
详细地,如果说我们要对颜色为$\{1,2,2,1,3,2,3\}$的序列$\{1,2,3,4,5,6,7\}$建立线段树,那么颜色$1$的线段树储存$\{1,0,0,4,0,0,0\}$,颜色为$2$的线段树储存$\{0,2,3,0,0,6,0\}$,颜色为$3$的线段树储存$\{0,0,0,0,5,0,7\}$.显然这样的话,空间复杂度会是$\rm O(N\times c)$的。但是我们动态开点,就变成了$\rm O(n\log n)$的啦。
修改权值就是在线段树上改,修改颜色就是把在原来颜色的线段树上的权值赋为$0$,再跑到新的颜色的线段树上去赋值。查询都是树剖+线段树的基本操作。
[Code](https://www.luogu.com.cn/paste/18hm4kjd)
## 例题13
[P2486 [SDOI2011]染色](https://www.luogu.com.cn/problem/P2486)
我个人认为比上面那一道题简单一点哈。
考虑**树剖**之后如何用**线段树**维护?
如果是在一个序列上应该怎么做呢?
合并两个序列的时候,需要考虑左序列的右端点和右序列的左端点的颜色是否相同,如果相同那么就要将整个区间的段数$-1$。线段树上合并两个节点的时候我们这样做,树上我们合并两个重链的时候我们也是保存当前两个点的颜色然后判断需不需要将答案$-1$即可。
[Code](https://www.luogu.com.cn/paste/8mgapay6)