数据结构相关
数据结构理论
维度
B维正交范围
对于一个
一维正交范围就是区间,二维正交范围是矩形,三维正交范围是立方体.
另外,如果
例如,找到区间
(lxl:我建议大家遇到题都要把能差分的东西差分到不能差分为止)
矩阵乘法归约
矩阵乘法
做
另外可以归约:
Example
Example1(链颜色数问题)
考虑构造一棵树:他有
Example2(区间逆序对)
考虑对序列和值域同时分块,考虑序列中第
Example3
平面上有若干点,两个操作:每次将横坐标小于等于
这玩意显然能加上扫描线归约区间逆序对.
数据结构
分块
Example1(luoguP8527 [Ynoi2003] 樋口円香)
首先将
不过我们先考虑个事:这么顺溜就出来了,为啥会需要分块啊?
首先看到题面的位移的形式,自然想到卷积.但问题在于有个区间,所以需要把区间处理掉.注意到每个区间是需要记录一下不同的
最后还没完,这题要平衡复杂度.
设块长为
但事实上FFT肯定是很慢的,所以我开到了
即使这样,笔者还是被卡常了(哭).
Example2(luogu[Ynoi2079] riapq)
首先对于这种区间内部贡献,而且每个点由前面点的贡献,先看有没有可差分性(区间逆序对也是一个套路).
注意到是有的,这样我们就把问题转化为了
先序列分块.然后
问题在于
现在的问题在于
其实挺好办的,因为散块要对一个区间有贡献,所以拿树状数组+差分统计一下就行.
最终复杂度为
如果你写完代码测一下会发现,跑的最慢的是散块对散块的贡献,你把sort改成基数排序就行.事实上实测了一下基数排序还不如直接换成树状数组.
但即使这样,笔者现在也没过这个题(哭).
Example3([CTS2022] 普罗霍洛夫卡)
比较复杂的分块题.
放弃了,太难了.
Example4(Walking Plan HDU 6331)
类似BSGS一样分块处理即可,最后需要枚举中继点,询问部分复杂度
Example5(P5063 [Ynoi2014] 置身天上之森)
考虑如果
但是
Example6(第二分块:[Ynoi2018]五彩斑斓的世界)
大概是对于每个块处理出它的值域范围:一开始是
二次离线
Example1(luoguP5047 [Ynoi2019 模拟赛] Yuno loves sqrt technology II)
简单来说就是区间逆序对数.
首先想到莫队,然后配一个树状数组就可以做到
那我们怎么改这个东西呢?
我们注意到:我们莫队在实现的无非是俩事:一个是移动左端点的时候判断左端点对右边的贡献,一个是移动右端点的时候,由于这俩是对称的,我们只讨论左端点不动移动右端点.
考虑这个过程的答案实际上是可差分的,因为
我们考虑对后者再进行一次离线操作,我们把这
做到这里其实要做完了,但还没完,这里空间复杂度达到了
注意到我们求出的是两个查询的答案的差分,最后还需要做一下前缀和求答案.
二维分块
我们现在有一个需要维护的
-
将平面分成
个 的 块,以 块为单位做二维前缀和. -
每个
块内部分成 个 的 块,在 块内部以 块为单位做二维前缀和. -
将整个平面横着分别分成一个个
的 块.(竖着也要分成一个个 的块,是类似的,略去) -
每个
块内部分成 个 个 块,在 块内部以 块为单位做二位前缀和.
注意到修改一个点的时候,需要更新三次二位前缀和,每次复杂度
查询显然是分四种情况讨论:
那散块怎么做呢?我们考虑一个特殊情况:修改点的纵坐标以及横坐标两两不同,或至少一个坐标只对应
如果查询的时候,也仍然是满足查询的一个
如果我们一开始不做二维前缀和,就可以实现
横着和竖着的散块相同,只讨论横着的.由于横着的散块高度
Example1(luoguP7448 [Ynoi2007] rdiq)
首先注意到这个问题严格难于区间逆序对,想到二次离线莫队.
开始做二次离线,发现问题在于我们需要求出右端点移动的时候,找到新增了多少个本质不同的逆序对.设上一个和
由于我们现在在保证左端点不动,于是我们考虑对于每种颜色,找到其在这个左端点后第一次出现的位置,并且只在这个位置贡献答案.这里其实已经可以扫描线了,套一下二次离线,把点扔到二位坐标系上.
现在问题在于,我们需要从
现在我们要查询的也就是左下角为
这个东西其实已经可以做高维前缀和了.为了使答案更显然,我们令
拆到这里发现其实到这一步
然后上二维分块.
Example2(luoguP8530 [Ynoi2003] 博丽灵梦)
首先自然的想法是拿莫队扫掉
这样我们的问题转化为:每次插入/删除一个点,求一个类似区间颜色数的东西.
那么这个东西咋做呢?
首先我们考虑插入/删除的本质,把第二维
有没有什么好办法?先考虑对矩形做加法然后单点查询这个操作看上去很蛋疼.我们考虑把它转化为单点加法矩形查询.这个做法比较显然:如果没有相同的只贡献一次的限制,我们就可以直接对于每个点
分析一下我们现在需要做的东西:
-
莫队时查询一个点的前驱后继,这个操作就需要
完成. -
次单点修改,这个操作需要 完成. -
次矩阵求和,这个操作需要在小于 的时间完成.
对于第一个问题,我们可能会想到用链表来解决.但问题在于链表难以支持插入操作.不过问题不大,我们有回滚莫队.这样就可以实现只删除不插入,解决了问题.
而后半部分是一个经典的二维分块.
简单来说,我们首先需要猜出时间复杂度为
trie树
Example1([2019zrtg十连测day1]set)
首先反应是扔到trie上然后异或就是打个tag,但是
更简单的做法是,我们考虑从小到大插入数字.这样异或几乎没有影响,但是
线段树
普通线段树
Example1(luoguP6780 [Ynoi2009] pmrllcsrms)
感觉这题比较厉害.
先扔做法:对
我们设
你注意到这个
仔细思考这个过程:线段树只可以维护有大于小于的限制的两个数,而不能维护和区间长度有关的条件.但如果一个限制和区间长度有关,可能可以通过翻转之类的操作取消掉区间长度.
这个问题解决了,我们再回到一开始:为啥要对
一方面,题目中的
一个需要注意的事是,由于我们最后查询的是一个区间,所以对于块间的处理是需要处理区间的.不过我选择将
但是啊,但是.我们发现我们一开始是需要把块间做线段树的那个
因为一开始这样会使得运算过程中有可能出现比
线段树分治
大概就是用到了线段树结构进行操作,通常用来处理存在区间的问题.
之所以说它是线段树分治而不是一般的分治,是因为有的时候我们还可以利用线段树的结构.
Example1([2022qbxt国庆Day1]dottlebot)
注意到每个点其实只需要找到
思考这个过程,我们将
线段树上二分
Example1([2022qbxt国庆Day3]analysis)
考虑全局的和是
先把数据离散化,那么这就是一个值域线段树上二分的过程.
另外值得一提的是,考虑树状数组的形态也即线段树删去所有的右儿子,因此树状数组上也是可以二分的.
Example2
给定
考虑先二分再贪心:二分一个值,然后看如果需要使得答案小于等于这个值,最少需要用多少次操作.这个咋做呢?一个想法是,我先从左到右去扫一遍,然后每次如果当前最大后缀和大于二分的
首先来看这个为什么是正确的.考虑后面的最大后缀和是会继承前面的最大后缀和的,因此让当前局面最小一定更优秀,并且每个位置选中的代价是相等的,那自然要选择贡献最高的那个.
显然,如果选择一个改掉的话,我们需要求出
那么什么样的
上面那个东西也就是:
这样就可以在交界点更新答案.
另外,我们实际上更新答案会用到实际上找到的最小的
线段树合并
线段树维护矩阵乘法
吉司机线段树
李超线段树
珂朵莉树
Example1(luoguP8512 [Ynoi Easy Round 2021] TEST_152)
首先有经典套路:赋值操作有用的只有最后一次.
所以考虑扫描线,扫右端点的时候直接用珂朵莉树做.这样就剩下左端点的问题,因为有珂朵莉树,所以再开以时间为下标的数据结构就能处理.
猫树
KD-Tree
处理
离线情况下通常可以用cdq分治代替.
如果要支持动态插点,可以使用复杂度不正确的替罪羊树重构+kdtree.
1D-Tree
也就是线段树.
2D-Tree
建树的时候,对于每一维轮流考虑,每次考虑将这一维上的坐标的中位数的点(基准点)找到,左右分治下去(下一层要考虑另一维)处理.查询和修改都是类似的.
struct KD_tree{
int son[2];
int x,y;
int siz;
int f;
int l,r,u,d;
}tr[qwq<<1|1];
int cur[qwq<<1|1];
int clen;
struct P{
int x,y;
}p[qwq<<1|1];
bool cmpx(P a,P b){
return a.x<b.x;
}
bool cmpy(P a,P b){
return a.y<b.y;
}
ll mabs(ll x){
if(x>0)return x;
return -x;
}
void pushup(int cnt){
tr[cnt].l=tr[cnt].r=tr[cnt].x;
tr[cnt].d=tr[cnt].u=tr[cnt].y;
tr[cnt].siz=1;
if(tr[cnt].son[0]){
tr[cnt].l=std::min(tr[cnt].l,tr[tr[cnt].son[0]].l);
tr[cnt].r=std::max(tr[cnt].r,tr[tr[cnt].son[0]].r);
tr[cnt].d=std::min(tr[cnt].d,tr[tr[cnt].son[0]].d);
tr[cnt].u=std::max(tr[cnt].u,tr[tr[cnt].son[0]].u);
tr[cnt].siz+=tr[tr[cnt].son[0]].siz;
}
if(tr[cnt].son[1]){
tr[cnt].l=std::min(tr[cnt].l,tr[tr[cnt].son[1]].l);
tr[cnt].r=std::max(tr[cnt].r,tr[tr[cnt].son[1]].r);
tr[cnt].d=std::min(tr[cnt].d,tr[tr[cnt].son[1]].d);
tr[cnt].u=std::max(tr[cnt].u,tr[tr[cnt].son[1]].u);
tr[cnt].siz+=tr[tr[cnt].son[1]].siz;
}
return ;
}
inline ll dispp(int pa,int pb){
return 1ll*mabs(tr[pa].x-tr[pb].x)+1ll*mabs(tr[pa].y-tr[pb].y);
}
inline ll dispm(int po,int mat){
if(!mat)return 320051113;
ll d=0;
if(tr[po].x<tr[mat].l)d+=tr[mat].l-tr[po].x;
if(tr[po].x>tr[mat].r)d+=tr[po].x-tr[mat].r;
if(tr[po].y<tr[mat].d)d+=tr[mat].d-tr[po].y;
if(tr[po].y>tr[mat].u)d+=tr[po].y-tr[mat].u;
return d;
}
bool get_var(int l,int r){
double avx=0,avy=0;
for(int i=l;i<=r;++i){
avx+=p[i].x;avy+=p[i].y;
}
avx/=(r-l+1);avy/=(r-l+1);
double varx=0,vary=0;
for(int i=l;i<=r;++i){
varx+=1ll*(avx-p[i].x)*(avx-p[i].x);
vary+=1ll*(avy-p[i].y)*(avy-p[i].y);
}
return varx<vary;
}
int build(int l,int r){
if(l>r)return 0;
int mid=(l+r)>>1;
int cnt=cur[mid];
tr[cnt].f=get_var(l,r);
if(tr[cnt].f)std::nth_element(p+l,p+mid,p+r+1,cmpy);
else std::nth_element(p+l,p+mid,p+r+1,cmpx);
tr[cnt].x=p[mid].x;tr[cnt].y=p[mid].y;tr[cnt].siz=1;
tr[cnt].son[0]=build(l,mid-1);
tr[cnt].son[1]=build(mid+1,r);
pushup(cnt);
return cnt;
}
笛卡尔树
Example1([CFgym101613]Factor-free tree)
首先有一个自然的想法是随便找一个和整个区间都互质的数,然后把序列分成左右两端向下递归.对于一棵构造出来的二叉树,它的复杂度就是
但我们考虑类似dsu on tree的做法,我们每次找到一个点,它将一个区间劈成了两部分,我们把小的那部分的贡献删去,然后做大的那部分.在递归过程中把大的那部分的贡献逐渐消磨掉.最后再做小的那部分,这样就类似于启发式合并的过程,复杂度就正确了.
Example2(23省选第一轮集训day5C)
注意到最小值的条件是容易满足的.
考虑枚举以每个点为最大值转移的区间,假设为
单调队列
Example(loj3151)
首先自然地,我们设
接下来咋优化咧?决策单调性!
嘶这题好像不满足决策单调性(这个故事也告诉我们不要看到
冷静一下,首先如果我把
树套树
解决矩阵修改+单点查询或单点修改+矩阵查询问题.
Example1
维护一个序列支持把
用树状数组维护平衡树,每次在树状数组上对应的节点修改即可.
Example2(Luogu4054 [JSOI2009]计数问题)
乍一看是动态三维问题.
相等维度是特殊的,我们开
数据结构常见套路
分开考虑
Example1(P6105 [Ynoi2010] y-fast trie)
考虑只有两种可能:
-
,取 作为答案. -
,取 作为答案.
后者只需要取出最大的两个数即可,至于前者,考虑将所有数字分成两个集合,一个集合只在
另外,
合并信息
lxl:这种问题主要需要解决三件事:标记对标记可合并,标记对值可合并,值与值可合并.
Example1([HNOI2011]括号修复 / [JSOI2011]括号序列)
注意到只要知道区间的最小前缀和以及区间的和,这个题就做完了.我们只需要维护这两件事.区间的和显然是好维护的,难以维护的是最小前缀和,我们来分开看每个操作:
替换:简单的.翻转:不太好做,尝试维护一下最小后缀和.反转:需要维护最大前缀和,进一步需要维护最大后缀和.
这样就可以更新答案了.
Example2(P4198 楼房重建)
左右维护单调栈合并,但这样复杂度肯定不对.
怎么办呢?我们可以用
Example3(CF1017G)
设
去除冗余信息
Example1(luoguP6617)
自然的想法是考虑找到每个点前面第一个和它之和为
我们注意到一个事实:我们也可以找到每个点后面第一个和它之和为
set维护颜色
Example1(luoguP5278 算术天才⑨与等差数列)
首先考虑
复杂度均摊
Example1(CF702F T-Shirts)
看到这个感觉很奇怪,想想好像也没有什么快速tag算法.
我们考虑对人建平衡树,然后按照顺序买衣服,每次找到所有能买这件衣服的人,显然是平衡树的某棵子树.但是,这棵子树在买完衣服后可能就不满足顺序了,那怎么办呢?能不能暴力重构一波?
事实上是可以的,对于一件价格为
Example2(uoj228)
一个自然的想法是暴力开根号,它会迅速缩短两个数之间的差.但可能也不能缩到
loj6029是等价做法.
Example3(Luogu 4690 [Ynoi2016]镜中的昆虫)
维护每个点的颜色相同的前驱,单点修改的话就是简单树套树.
然后区间推平可以用颜色块均摊(同一个颜色块内只需要改开头元素,剩下的都是
根号分治
Example1(luoguP7722 [Ynoi2007] tmpq)
这个题告诉我们一个故事:有的时候,有的条件可能真的没用.
直接把题目改成:每次修改
Example2
对于一个数字
注意到如果
Example3
给定一棵树,每次修改树上某个点的权值,或询问某个点周围的点的权值和.
度数大的点在修改的时候改,度数小的在询问的时候做.
Example4
给定序列,每次询问给出两个数字
对于出现次数大的,处理出它和所有数字的答案.
如果
Example5(SHOI2006 Homework)
首先对于
对于
Example6
给定
注意到
首先有一个性质:对于一对点
接下来,对于出现次数大于
对于出现次数小于
重链分治
Example1(Luogu5314 [Ynoi2011]ODT)
其实不是根号分治,但是差不多,扔这里了.
给一棵树,边权为
每个点周围的点一共有三种可能:父亲,重儿子,轻儿子,特判重儿子和父亲,然后处理出所有轻儿子的情况,这个怎么做都能做(大不了把所有轻儿子全扔平衡树里),然后重链剖分的时候只会改
扫描线
一维扫描线
最经典的应用是对于一个
主席树通常就是解决强制在线不能处理扫描线的问题.
另外,通常认为时间也是一维,也就是即使是动态问题也一般是等价于对时间跑了扫描线.
二维扫描线
也就是莫队.
Example
Example1(CF1609F Interesting Sections)
首先枚举每个数的
可以求出每个点
Example2(CF833E)
离散化,设
先考虑
如果
先考虑无交的情况,这个时候答案显然是
再考虑有交的情况,答案应该为
那么怎么判断两朵云有交呢?我们不用判断两朵云是否有交,因为前者一定没有后者优秀.不过需要判断两朵云不能是同一朵,这个存一下次大值就可以解决.
这样就转移完了这个题,挺厉害的.
Example3(loj3489)
时间也是一维,扫序列维护时间,线段树二分就可以解决.
具体地,我们需要对每个询问找到这个询问前最近的队列为空的时刻,然后这个时刻后面的答案就可以直接拿前缀max二分,问题在于怎么求这个时刻.
这个时刻也是好求的,它一定是前缀的最小值(这个点一定清空了,这个点后面的数比它小,因此这个点变成
Example4(luoguP7709 「Wdsr-2.7」八云蓝自动机 Ⅱ)
如果初始序列全为
倒着扫操作序列,维护当前还没有得到答案的询问,每次找到一个操作一定将整个区间的询问全部得知了答案.
不然不会做.
Example5(luogu3863)
仍然是个数据结构维护时间维,扫描线扫序列维的东西.
Example6(qoj6304)
考虑横纵坐标是对称的,因此我们只需要考虑两横一竖的情况和三条横的情况.
先做三条横,枚举中间的那个横的位置,剩了一段前缀和一段后缀需要覆盖,这个可以前后缀预处理.
然后是两横一竖,扫竖线,问题转化为动态加入删除区间,求当前用两个点覆盖所有区间的方案数,不妨设这两个点是
注意到
莫队
回滚莫队
带修莫队
也就是维护三维的扫描线,根据KDT不难发现复杂度是
树上莫队
二次离线莫队
这个直接拿区间逆序对当例子记笔记好了.
如果我们用正常的莫队做区间逆序对,我们会得到带个
那么怎么解决这个问题呢?我们现在无非是有
Example
Example1([Ynoi2016]这是我自己的发明)
dfn将子树转序列,注意到换根无非是把一个序列拆成了两个序列,这是好做的.不过这玩意都
Example2([HNOI2016]大数)
区间子区间问题对于莫队是有一个套路的:即转化为二元组计数问题.
具体怎么做呢?首先这个题我们特判掉
Example3(luoguP3604 美好的每一天)
类似上面那个题,用哈希(其实就是将26个字母表示成26个二的幂次)然后异或起来,和上面的题就完全一样了,做二元组计数.
区间子区间问题
求有多少个子区间满足条件.
上二维平面,子区间所代表的
Example1(CF997E)
考虑转化为二维平面,
另外这里的矩阵加法有
时间倒流
Example1([2022qbxt国庆Day6]sgtbeats)
首先考虑:如果一个点被清空了多次,那么只有最后一次有意义.
删除操作很难做,考虑变成插入,然后就可以拿数据结构维护操作序列的后缀max,存一下每个点最后被清空的时间,然后处理即可.
Example2([WC2006]水管局长)
时间倒流,删边变加边,LCT做一下.
数据结构维护分段函数
Example1(CF1540D Inverse Inversions)
考虑对于一个数列怎么构造:假设只考虑前
那么我们现在要知道
我们将数列分块,设块长为
考虑暴力求出这个分段函数,每次询问的时候直接二分,修改的时候考虑每个块维护一个线段树,线段树的区间表示这个区间对应的分段函数.这样单点修改复杂度是
于是最后复杂度为
根号平衡
根号平衡主要用到下面四个东西:
-
单点加, 区间和:维护块内的和即可. -
单点加, 区间和:维护块内和块间的前缀和即可. -
区间加, 单点和:差分转化为 .当然打标记也是可以的. -
区间加, 单点和:差分转化为 .当然打标记也是可以的.
还有一些拓展的东西:
-
维护值域
的集合,支持 插入, 查询第 小:值域分块就可以. -
维护值域
的集合,支持 插入, 查询第 小:值域分块,然后暴力改变每个点所属的块就行.
Example
Example1(区间众数)
首先分块,处理出
但是可以优化,我们设
不删除莫队也能做.
当然,如果只要求区间众数的出现次数,可以直接莫队.
Example2(CodeChef Chef and Churu)
首先发现函数是不会被修改的,因此考虑对函数分块,对于那些散着的函数肯定可以用一个
而怎么快速处理整块呢?发现函数可差分,差分后就可以算出每一个位置对这个块内的总贡献,这样就可以更新了.
Example3([Ahoi2013]作业)
莫队,发现有
Example4(Bzoj4241历史研究)
回滚莫队板子.
事实上考虑可能的答案只有
评论