基础数据结构学习笔记
基础数据结构学习笔记
来自\(\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
有一堆类似于\(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\),所以要离散化。
例题2
\(\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\).
因此我们可以想到这题可以使用普通队列来做,使复杂度降至\(\rm O(m)\)。
使用3个队列分别保存没被切过的蚯蚓,被切过的蚯蚓左段,和被切过的蚯蚓右段。
每次取出3个队列中最长的一只蚯蚓,将其切断都分别放入后两个数组,依此类推循环\(m\)次即可求出结果。
还需要考虑蚯蚓的增长长度问题,由于每次除了被切的蚯蚓,其他所有的蚯蚓都在增长,所以只需要再从队列中取元素的时候加上\((i-1)\times q\),放回时减去\(i\times q\)的长度(因为被切的蚯蚓这年不增长,所以多减\(q\)厘米,具体实现参见程序)。
例题3
给你一个长度为\(n(n\le10^6)\)的数列,有\(T(T\le 10^3)\)组询问,每一组询问查询区间\([l,r]\),请选择一个\(x\),将所有\(\le x\)的数标记后得到的段数\(\in[l,r]\),得分为每一段连续标记的区间的长度的平方和除以\(x\),最大化得分。
\(0\le a_i\le 10^6\).
强制在线。
注意到没有修改,那么就可以愉快的预处理。
首先从小到大枚举\(x(1\le x\le 10^6)\)同时用并查集维护连通性,计算答案为\(x\)时有多少段,得分是多少。
把这个\((得分,x)\)二元组放到一个数组中(下标为段数),用线段树维护即可。
结果大概可能是像我一样辛辛苦苦打完线段树,发现线段树无论如何都会\(MLE\)……线段树的空间复杂度是\(4\times n\)左右,而分块的空间复杂度只有\(n+\sqrt{n}\),于是换成分块就可以愉快的AC啦!这是一种时间换空间的方法。
例题4
首先一段长度为\(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}\)。
例题5
发现原式\(\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]]\)的逆序对即可。
这里可以用树状数组或者归并排序来计算逆序对。
例题6
求区间出现次数\(\ge 2\)的数的个数。
\(n\le 3\times 10^5\)
显然这不就是莫队直接过嘛
\(n\le 2\times 10^6\)
这还不就是莫队,卡卡常再来个指令集
\(Subtask1\)就不在这里多讲述了,重点讲述\(Subtask2\).
注意到选择的区间中每个数第二次出现才会有\(1\)的贡献,那么我们不妨就把\(1\)放在每个数第二次出现的地方。
类似于莫队的思想,但是这一次我们只按照左端点 \(l\) 来排序,方便统计贡献。
预先处理好每个数的后继(这里指的是下一次出现该数的位置),然后 \(l\) 指针每移动一位,就处理好贡献转移:
- \(l\)这一位走出了区间,那么我们应当给\(nxt_l\)的贡献\(-1\)(因为他已经不是区间中第二次出现的数了,变成第一次出现的了)
- 既然\(nxt_l\)变成了\(a[l]\)第一次出现在该区间中的位置,那么相应的,\(nxt_{nxt_l}\)就会变成区间中第二次出现的数,获得贡献\(+1\)
初始化是就先将\([1,n]\)中第二次出现的数的贡献\(+1\)。
最后问的是\([l,r]\),那么就用树状数组维护前缀和的贡献,差分出区间的贡献即可。
例题7
在一个平面内,询问所有的高度大于\(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);
}
例题8
先想两种极端的做法
- 对于每个询问,暴力计算\(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\)
例题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)\)。
加强版
蒲公英的数据范围十分友好:\(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++
。
若在右侧则同理。
小节
分块模板题的训练就到此完结了。
但是分块的玄学美妙优雅暴力之处不仅仅于此。
比如根号平衡等。
比如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\)的模拟赛吧,就没有那么卡时间。
但是遇到毒瘤题,根号平衡是一种很重要的卡常方法。
以后遇到了卡常分块的话可以记录一下。
例题10
请写一个程序,要求维护一个数列,支持以下\(6\)种操作:
- 在第\(pos\)个数字后面插入\(tot\)个数字
- 从第\(pos\)个数字开始删除\(tot\)个数字
- 从第\(pos\)个数字开始覆盖\(tot\)个数字为\(c\)
- 从第\(pos\)个数字开始的\(tot\)个数字翻转
- 区间和
- 最大子段和
\(|a|\le 5\times 10^5,1\le m\le 2\times 10^4,a_i\in[-10^3,10^3],\sum_i^na_i\le 4\times 10^6\).
显然用平衡树。这里使用\(FHQ-Treap\)。
- 插入一堆数字,先对这一堆数字进行\(\rm O(n)\)的建树,然后
split+merge
。 - 删除一堆数字,
split
后delete
。 - 区间覆盖,
split
后打标记。 - 区间翻转,
split
后打标记。 - 区间和,平衡树基本操作,维护\(sum\)。
- 最大子段和,平衡树基本操作\(lmax,rmax,zmax\)。
所以这道题就是经典的平衡树\(\color{white}SB\)码农题了,使用\(FHQ-Treap\)或\(Splay\)即可。
如果用数组的话需要写垃圾回收,指针版就没有这么多麻烦,直接delete
就好。
例题11\(\color{gray}(报复社会)\)
对一个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}\)按位与的值
- 查询区间和
强制在线。
\(1\le n,m\le 3\times10^6,0\le a_i\le1\).
手写珂朵莉树模板题,建议只要会珂朵莉树(哪怕没有写过珂朵莉树的都行),写过非旋\(Treap\)的都可以来试一试。
思路很简单的,代码也不短,可以练练手吧。如果实在是不会的话就给我点个赞然后看看题解吧
按照传统(?),只给部分代码供参考。有问题也可以私信(如果我还没AFO的话)
唉,2021年了,这道题需要一点卡常了……
原本\(Ynoi\)是很友好的,但是有些人用错误的时间复杂度乱搞过去了,所以只能加强加强再加强,导致正解想要过去也比较困难了……
\(\textbf{2021-03-31 08:00:25 thus,AC.}\)
附上debug日志。祝您早日AC。
例题12
树上每个点都有其颜色和权值。单点修改一个点的颜色或价值,查询树上一条链间颜色与起点/终点相同的点的权值之和/最大值。
对原树进行树剖,对每一种颜色建立动态开点线段树。
详细地,如果说我们要对颜色为\(\{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\),再跑到新的颜色的线段树上去赋值。查询都是树剖+线段树的基本操作。
例题13
我个人认为比上面那一道题简单一点哈。
考虑树剖之后如何用线段树维护?
如果是在一个序列上应该怎么做呢?
合并两个序列的时候,需要考虑左序列的右端点和右序列的左端点的颜色是否相同,如果相同那么就要将整个区间的段数\(-1\)。线段树上合并两个节点的时候我们这样做,树上我们合并两个重链的时候我们也是保存当前两个点的颜色然后判断需不需要将答案\(-1\)即可。
算法竞赛进阶指南 0x44 ↩︎