一.对于数据结构题目的基本介绍
1.数据结构问题:没有高级低级之分,只有好不好用,该选择什么 简称为DS
2.一般数据结构题n,m不大于10e5(一般是需要O(nlogn) 当读入的规模达到 10e6的时候,最好时候fread优化
https://blog.csdn.net/weixin_30755709/article/details/95250556?utm_medium=distribute.pc_relevant.none-task-blog-baidulandingword-1&spm=1001.2101.3001.4242(各种读入优化和比较 )
二.前置知识
1.基本方法:前缀和 差分 离散化 离线(也就是把问题存下来) 二分(单调性,可选性) 倍增永 双指针 标记久化(就是计算所有
2.基本数据结构:线段树(特定属性) 树状数组(维护高维) st表 单调队列 单调栈 并查集
3.求lca 两种 3.1O(n) -O(longn)【树链剖分】(倍增太慢了) 3.21O(nlongn)-O(1)(动态点分支)把lca转成rmq的问题 dfs序【字数标号,结合书链剖分】和链差分
三.各类数据结构介绍
一.线段树:对序列递归的求重点形成两部分的分治树,任何以单点为基本单位的分治树都可以适配到任何区间,区重点保证树高为(logn)切合并所有被完整包含的极大区间就可以得到目标区间
【补充:log级别数据结构 ->都是分治树(也就是左右不一定平衡)】
建线段树 然后把右儿子来储存(把长度扩展乘2的幂)BIT(也就是不需要任何右儿子的节点)
2.差分:等差数列可以二级差分:也就是可以区间加一个数,然后查询区间和(就可以使用树状数组来实现)
3.双指针(也叫尺取法):区间上本来的套路是枚举左端点,加上合法的右端点的数 也就是说左端点是单调的
4.单调队列:后面插入元素,删除最前面的元素,求序列元素的最小值,【也就是队尾更大的元素不再产生贡献,可以直接删去,】可以结合双指针使用(也就是根本不需要维护这个序列)【把两个指针堪称序列】
给一个序列和数l,对所有长度为l的区间,分别求出区间最小值
5.单调栈:经典问题:对序列a和下标i,求出ai之后第一个大于ai的元素的下标【也就是以ai为最大值的极大区间
二.并查集:性质如下
1.维护不相交集合,通常使用路径压缩策略,也就是指压缩一半的路径 DSU
2.按质合并是严格的
3.也有带权的并查集,维护点权查,可以用取模后的权值差来分类(有的时候可以用LCT)
反转时间:给一张图,每次操作一个点,求连通块:倒着处理,插入变删除,用并查集
【翻转思想】如果可以离线的话,我们可以吧删除边插入,插入变删除 强制在线就算了吧
【倒序思想】:面对之后的问题,我们思考是不是可以通过逆序等价或者相反的操作得到等价的答案
P3369(baozi题)
treap支持操作(维护集合,没有顺序关系)
1.支持操作:插入x,删除x,查询小于x的数的个数,查询第k小的数
二叉搜索树:每个节点都有一个关键字,左边都比他小,右边都比他大【中序遍历单调不降】BST
平衡树;深度为O(logn)的二叉搜索树 splay(自适应二叉搜索树)treap(期望的二叉搜索树) AVL 红黑 替罪羊树
TREAP:给每个节点随机分配一个权值,调整树的形态满足堆性质 期望深度
1.哨兵节点:c++作为叶子结点儿子的虚节点(设置为虚节点,更新就爆炸了)哨兵节点size=0,ccnt=0,更新它也没有任何问题【维护序列可能要在序列两端各放一个】原本需要特殊分配
数组名字可以转化为首地址的指针,也就是建一个哨兵节点的名字的数组,全局变量会被值初始化为0,这样哨兵节点就不需要初始化。用一个last节点来分配空间
【补充:加入你要初始化数组的首元素,一定要在main里面初始化】std也不要
2.操作 左旋:把根节点旋转到左边叫左旋 zag 右旋叫zig
![]()
![]()
![]()
自己指向右儿子的左子树,右儿子的左子针改为它,那么他就会被旋上来了(这里要引用要更新)
和平衡树的区别就是多了一个本节点自己的信息
2.初始化:一般可以直接写列表,构造函数麻烦死掉了
【补充说明 rand】注意一下:windows里面的rand一般只有1-32767(有的时候也可以写一些更快的的手写rand,也就是没有那么随机
2.1 保证随机的写法:rand*(RAND_MAX+1)+rand()或者说rand()*rand()
【补充 RAND_MAX】RAND_MAX是C中stdlib.h中宏定义的一个字符常量:
#define RAND_MAX Ox7FFF
其值最小为32767,最大为2147483647
通常在产生随机小数时可以使用RAND_MAX。
所以用double 转换然后再加1 防止溢出
((double) rand() / ((double) RAND_MAX + 1)
不能用加完再类型转换 已经丢失了精度
【有的时候可以使用懒惰删除【红黑树直接写个c=0】】treap懒惰删除不大
【也就是treap可以直接加树状数组啥的】
三:权值线段树 :
设cx为x的出现次数,建线段树维护c的前缀和,由于c的下标是权值,所以这样的线段树通常称为权值线段树
四.treap
1.相当于说插入一个x就是cx+1,这个时候就可以用权值线段树来写
2.求第k小,就可以使用线段树二分:也就是在分治树上自顶而下的移动
(处理左儿子的size出现次数合,大于往左,小于往右)【线段树可以动态开点,但树状数组必须离散化;
3.一定要写一下两次二分【查k小的题目,都是和左边儿子的size有关【也就是甚至每个节点存储他左二子的size但不存储自己的size】】
4.动态开点:没有点,不建树,修改到不存的点再创建,或查询到不存在就可以直接算出来【适用于无法离散化的情况】(每次最多创建O(nlogw),w是值域范围【平衡树空间更简单】【线段树做一些区间打标记的操作更简单】
5.维护序列:
5.1.指定位置的插入和删除(分裂合并线段树,除此线段树就一般做不了)
5.2 区间染色(珂朵莉树和OTD可能会卡掉) 5.3 翻转区间 5.4 求区间和 5.5 求整个数列的最大字段和【改成区间也是一样的】
维护序列:合并两个区间的最值区间,只有两种情况:位于某一个区间内,或者横跨两个区间;维护后缀信息:左右子树就可以合并;【历史最大值】【再维护最大前缀和后缀】
所以我们维护的其实是一个有序表(原则:能一个log作的没有必要用一个根号作)
解法:块状链表(没什么用)
6.SPALY:伸展:使最后一个被访问的节点为根节点
复杂度:总复杂 O(nlogn) 均摊为O(logn)
1.旋转操作:四种旋转操作,判断和父亲节点和爷爷节点的关系是否相同【也就是它的常数是非常大的】
zigzig和zigzag写的时候可以精简代码【找一个LCT的板子然后后求一个SPLAY】
2.正常是自底向上splay(x向上,也就是要把x向上的点的标记要全部pushdown)(也可以自顶向下splay
【凭自己的splay找个baozi】
3.splay作区间的题目,先splay到根,再把后继splay到后继的根,(也就是形成一个完整的树或者空树)
treap难以O(n)建树,所以一般一个一个插
7.无旋treap
【所有二叉都可以分裂(相当于把左子树和右字数拆开)【合并必须一个树的所有关键字都要比另一个树的关键字小】,但是treap可以合并(维护堆性质)复杂度并不会改变,也就是插入和删除都可以用分裂和合并来实现】
插入:找个x位置分类,x位置单独建树来三个树来合并;(可以维护序列,完全递归实现,无旋,FHQ,可持久化treap,函数式treap)
【也就是序列维护的是维护下标,但是下标是动态的,我们相当于是通过szie来维护相对的下标】
【一个树的中序遍历就是对应的相关区间】
8.翻转区间:交换根的左右子树,然后反转左右子树的相对区间(打反转标记就可以)
总结一下:使用数据结构 的其中一个技巧是要观察所储存的数据的性质,看一看是否可以进行合并,如果不能的会就需要分别储存更多的二级数据或者换一种数据结构
【数据结构的熟练是非常重要的】P2042 模板题
:常数treap《splay《无旋
1.splay维护有序表效率比较低,实现不方便(节点少的时候更慢【维护有序表更慢】
2.splay常用于LCT或者维护序列
3.无旋treap可以维护序列实现可持久化【简单来说,就是可以保存历史版本的数据结构】
最麻烦的:树上问题,熟练度
三.高维问题:
1.1 维度的问题:操作位置有限制,就有位置轴,权值限制,就是权值轴,儿操作的顺序限制,就有时间轴;
位置和权值轴没有本质区别
1.2时间轴变化:P3755 二维数点 将某一轴视为时间轴,按照时间顺序插入,【也就是某一个矩形可以拆成四个矩形】,将前缀查询转化为某个时间上降维
1.3本质降维:
1.离线扫描:将所有的询问和数据集合按新的时间轴排序(也就是所谓的扫描线)
2.在线:可持久化,查询历史版本(如果支持历史版本修改就是完全可持久化),方法一般是path copying【一次新建一条链,空间O(nlogn);
【也就是再修改的时候吧新建的点复制一个全新的点出来,如果说这个点修改了,那么复制的点指向的也是他的对应的儿子,但是原先的节点的儿子仍旧是一样的,因此就可以做到可持久化;函数式:原本的不能修改;常用于保存动态的插入元素的求解;如果可以修改历史版本,就可以保存树形的插入过程:【求链k小】
fat node:每一个点开一个动态表,在上面查询时间戳;
【也就是能将维就一定要降维,除了我们需要
P1972\4113 不同的数的个数:本身不可以合并(基本的原则:可以用log不用根号)都是维护前驱;
三维数点:1.P3810 也就是查一个三维的前缀和,三维偏序【分cdq分治】如果说时间轴没有给出卫视通过时间轴变化的过程,那么一定是可以离线的
2.树套树:维护某一维的基本方法是分治树:分显式和隐式的;
树状数组不能套树状数组:(也就是树状数组二维也得离散化,没有什么实用性)
1.1树状数组套平衡树 空间O(nlogn)
3.常说的分治使用的是隐式的分治树
(也就是说所有的细节都需要自己来考虑)
CDQ分治:通过分治维护时间堆:把询问分成两半,求解前一段对后一段的贡献【也就是每一次都可以加上左边的一段】【找个baozi】通常查询时间前缀,也可以做到查询时间区间(实现比较繁琐)】【时间维排序下来是一定需要离线的了】
4.整体二分
:用分治维护需要二分的维度,在分治树上二分;k小这个东西可能需要二分(如果套平衡树的话就很麻烦)
5.分治树的组合:
1.树桩数组套平衡树:空间 O(n log n),效率一般【好写,在线】
2.【所谓的带修主席树】树状数组套线段树:空间 O(n log2 n),效率一般,各线段树可以同时二分
线段树套树:支持不可减外层信息
分治套树状数组:离线,空间 O(n),效率很高,可以在隐式分治树上二分【隐式的好处是你没有必要所有东西都全部存储下来,都是用完就扔的】【不直观】
分治套线段树:离线,空间 O(n),效率较高,支持较复杂的操作【常规的线段树信息,用分治搞掉外面那一维】
树状数组套压缩 trie:空间 O(n log n),实现难度较大
“重量”平衡树套树:支持外层插入,实现难度较大
【待修改的题目本身就是不代修改的题目进行时间变换过后的结果】【也就是我们需要确定要维护的维度】
6.KDT 树:查询圆上最小值 维护k维空间的信息 O(n^1-(1/k))空间O(n)并且支持高维标记 不能剪枝的时候常数巨大;【平衡树上修改区间】
【总结:对于某些高维的问题,不妨将其转化成三维,二维的数点模型就可以了】
根号方法:
1.莫队:
1.操作:每次给出l和r求有多少个ij是相等的(n根号m)[是可以基数排序的,一般用sort就可以】(mlogm的排序一般是没有关系的)
2.概念:将序列分15:33:45块,询问将左端点所在块为第一关键字,右端点为第二关键字来排序,一次处理询问【离线】,维护一个空的区间(l=1,r=0,左端点大于右端点),每次插入或删除相应的区间【每次按序】
3.块的大小
P1494 #理解下来,在不断插入的过程中,每插入一个都会合之前已有序列中同样数字的个数形成有序对,答案直接++并且移动断电就可以了 :步骤就是转移端点,插入,计算贡献就可以了
4.时间复杂度:【因为是排序过后的】设块的大小为l,则左端点移动不超过ml+2n,右端点不超过2n(n/l也就是块的个数)那么最优的复杂度为O(n根号m)此时l=n/根号m取最大值
【排序可以按照奇偶性来排序,这样同时或许可以节省右端点左移一倍的常数】
5.简单的分块:块的大小直接用 bsize =n/sqrt(m*2/3);理论上是最优的 块的个数是:根号m
6.曼哈顿最小生成树也会使用这样
7.回文莫队:无法进行删除操作,可以只从某个块的左端点(分类),某个块的左端点开始维护区间,右端点单调移动,只在有段插入,移动到每个询问的右端点(相当于右边已经搞完了)再重新把第i块插入,然后再撤销左端点插入;【每一个询问都插入再撤销】但是要支持从两端插入
简单查询:一般都是log-log ,根号log是非常慢的 O(根号n)-O(1)或者O(1)-O(根号n)可以进行转化
1.序列单点修改 区间和 (转化为块前缀和以及根号个块的块内前缀和作一个前缀和)【暴力修改两个前缀和】 n-1
块维护一个块内的和 查询所有的前缀块+块内的暴力查询
【本质上是在块内和块外把他转化为常数个,对于分块而言,基本上就是讨论在块中,块外,块左,块右之间的联系】
2.序列区间加 : 每一个区间加可以分成左边(部分) 右边(部分) 中间多个块打标记 统计的时候加标记
3.集合插入
1-n 维护数的出现次数 按照权值分块 暴力查询k小(块内一个一个枚举)