进阶数据结构学习笔记
进阶数据结构学习笔记
不妨先看看前一篇。
像上一篇一样,先列出用到的高级算法/数据结构/思想:
- 线段树合并
1,5,6
- 可持久化
2,3,4,7
- 线段树上二分
4,5,8
- 线段树分裂
6
- 二分答案
6
- 贪心
7
- 堆
8
下面是例题时间!
例题1
P4556 [Vani有约会]雨天的尾巴 /【模板】线段树合并
给你一棵树,每个节点都是有一个可重集合,每次选择一条链,向链上的每一个点的集合中都插入一个数。最后输出每个点的集合中出现次数最多的数,若有相同的输出小的。
我们将每一个修改树上差分后变成为,即我们在上各打上一个的标记,在上打一个,在上也打上一个即可。
这个标记我们使用动态开点的权值线段树维护。对每一个点都开一颗权值线段树,然后对于每次修改进行单点修改同时上传标记是维护最大值。
最后统计答案,从叶子节点向上合并线段树。线段树的合并很简单,就是将一棵线段树的有用的节点的数据合并即可。
例题2
继续使用动态开点线段树,记第次修改/询问的根节点为。
修改时,我们一般的线段树使用时是形如change(int p,int l,int r,...)
这样的方式。对于需要拷贝节点的动态开点线段树呢,我们就使用change(int &p,int pre,int l,int r,...)
,表示当前节点p
如果需要修改的话应当从pre
节点拷贝过来。
代码片段:
void change(int &p,int pre,int l,int r,int x,int v)
{
p=copy(pre);
if(l==r)
{
val(p)=v;
return;
}
int mid=l+r>>1;
if(x<=mid)
{
change(ls(p),ls(pre),l,mid,x,v);
}
else
{
change(rs(p),rs(pre),mid+1,r,x,v);
}
}
例题3
可持久化并查集基于可持久化数组。
并查集的最根本的操作是查询一个点的父亲,即,可以看作是一个数组的第位。但是我们使用并查集是还需要用到一些优化比如按秩合并,路径压缩等。在路径压缩中,查询操作中的修改太多了,我们放弃这一种方式,而转念想一想,路径压缩是否可以用于可持久化并查集?当然可以。
在并查集上我们需要修改的父亲(不是指树的根,是直接的父亲),只需要f[x]=y
。但是为了可持久化我们就损耗一定的时间(换取每一次版本保存的时间),在上的线段树进行可持久化数组那样的change(int &p,int pre,int l,int y,int pos)
操作。查询f[x]
也是如此(但是就不用修改了,不需要引用啥的)。
并查集还有一个操作是getf(int x)
,即得到所在的树的树根。我们暴力向上跳(用上面的那种,每查询一次父亲的时间复杂度是的),直到则返回。
merge
操作是基于getf
和change
的,就不用说了。
ask
操作就getf(x)==getf(y)
即可。
注意数组要开大一点,一次修改操作的时间复杂度&空间复杂度都是的,所以总时间复杂度就是的。
再注意一点,change
操作中,如果修改的左子树,就要把右子树直接copy过来,反之同理。所以我先copy,再进行下一层的change
,反正下一层一定会新建节点的。
依照pre所在的旧版本,将p所在的新版本中的的父亲设置成.
void change(int &p,int pre,int l,int r,int x,int v)
{
p=++cnt;
if(l==r)
{
f[p]=v;
dep[p]=dep[pre];
return;
}
int mid=l+r>>1;
ls[p]=ls[pre];//
rs[p]=rs[pre];//
if(x<=mid) change(ls[p],ls[pre],l,mid,x,v);
else change(rs[p],rs[pre],mid+1,r,x,v);
}
例题4
经典的问题——静态区间第k小。
我们对序列上每一个点的前缀(记为)建立一棵动态开点权值线段树,然而发现相邻的两颗树之间只有微小的差距,有些节点是不变的。所以在建立时,我们就基于,相同的节点直接复制,有修改的节点就新建节点。同时维护权值线段树上的节点所表示的可以中有多少个数字。
查询时,我们将询问差分为和,然后就同时在两颗线段树上进行线段树上二分。
详细地,假设当前节点是和,其所代表的区间皆为。那么区间中就有个数字。假设现在是要求这个区间的第小数,那么如若,则第小数在的左子区间中;否则就在右子区间中。
我自己写了一个bug:(我是菜鸡,大佬勿喷,仅供个人记录)
void change(int &p,int pre,int l,int r,int x)
{
if(!p) p=node(pre);//
if(l==r)
{
sum[p]++;
return;
}
int mid=(l+r)>>1;
if(x<=mid) change(ls[p],ls[pre],l,mid,x);
else change(rs[p],rs[pre],mid+1,r,x);
upd(p);
}
这种写法中,无论如何p都应该要从pre复制过来,不然要是一个点的ls不为0,向左儿子change时,就会直接在旧版本上change了。
正确写法:
void change(int &p,int pre,int l,int r,int x)
{
p=node(pre);
if(l==r)
{
sum[p]++;
return;
}
int mid=(l+r)>>1;
if(x<=mid) change(ls[p],ls[pre],l,mid,x);
else change(rs[p],rs[pre],mid+1,r,x);
upd(p);
}
例题5
有一堆点,每个点都有点权(保证没有两个点的点权是相同的,反正就是直接给排名啦)。每次有两个操作,一是将两个点之间连边,二是询问一个点所在的连通块中排名第小的点的编号,若不存在则输出。
这两个操作感觉很模板:合并两个连通块,用并查集维护连通性,然后对于排名可以用线段树合并来维护;询问排名第小,就类比P3834 【模板】可持久化线段树 2(主席树)中的线段树二分即可。
例题6
给你一个序列,有次操作,每一次操作将区间重新排序(0
是升序,1
是降序)。最后输出。
.
看见只有一个询问,当然是从这里入手了。
考虑二分答案:假如我们已经知道了修改最后,,那么我们可以二分这个不是嘛。如何check是否合法呢?
首先要知道一个:可以发现对序列排序就相当于将所有全部放到了的左边,也就相当于先记录这个区间的,后将覆盖为,将覆盖为,这个可以用线段树在的时间复杂度内完成一次修改。
如果我们已经知道了答案是,那么我们不在关心其他一对值之间的大小关系,只关心其他值与的相对关系。所以我们将小于的所有数全部变成,大于等于的数全部变成1,然后看是否为,是的话说明可能偏大。否则一定偏小。
当然也有用线段树分裂&合并在的时间复杂度内处理多组询问的算法,这里继续留白。
例题7
给你一个序列,每一次询问
看到这个题面,不妨先想一想最简单的没有这些参数我们可以暴力扫建立一棵0/1
trie然后在trie上面贪心向1
的那一条边走,没有1
边才走0
边。
加了一个怎么办?看看的当前一位如果是0
的话还是优先向1
否则优先向0
就是了。
有了怎么办,拿主席树来维护吧!将两个线段树相减就可以得到一段区间中的数了。查询是否有1这一条边,发现在数值上相当于查询区间中是否存在数,这个就可以用线段树查询了!
有了个?每次查询的时候加上个不就好了?
最后算出来的答案别忘记.
例题8
给你一个序列,定义一段区间的价值为每次询问区间中前大的不同区间的价值之和。
.
既然是要选择区间,那么我们将区间转换为前缀形式,即设,则。
现在题目变成:给定一个数组,且,求时的前大种取值之和。
可以变成,最后选择两倍的并将和除以二即可。当时,一定不会成为答案,所以这个约束也可以去掉。
现在题目变成:给定一个数组,且,求的前大种取值之和的一半。
如果把插入到01 trie中,那么没给定一个,我们可以在的时间复杂度内找到与一个数异或结果第大的数。方法类似于线段树上二分,可参见例题4和例题5。
这时,我们用一个大根堆来计算答案。堆开始时放入与中的元素异或的最大值(用查询)同时保存信息和。每一次取出堆顶,假设堆顶是,把加入答案,找到与中的元素异或的第大值变成新的并将放入堆中。进行次操作,那么取出的依次就是中元素两两异或的前大值,再除以二即可。(我个人感觉这部分的思路来自一种sb单调性,同P2048 [NOI2010] 超级钢琴)
时间复杂度。
注意trie的空间请尽可能的开大,越大越好,只要不MLE,就往死里开。
一般开上倍就差不多了
例题9
LCT,一种基于splay的数据结构,用来动态处理树的形态和连通性问题。
例题10
很巧妙的将一个奇怪的问题转化成了树上问题!
例题11
支持两个操作:合并两个小根堆,查询第个数所在的堆的堆顶并将其删除。
首先是并查集的找父亲:
int getf(int x)
{
if(f(x)==x) return x;
return f(x)=getf(f(x));
}
左偏树的合并很像,因为他们都是基于堆的,比较关键字的优先级。
int merge(int x,int y)
{
if(!x) return y;
if(!y) return x;
if(v(x)>v(y)||(v(x)==v(y)&&x>y)) swap(x,y);//let x<y
rs(x)=merge(rs(x),y);
if(dist(rs(x))>dist(ls(x))) swap(ls(x),rs(x));//liftist heap
f(ls(x))=f(rs(x))=f(x)=x;
if(!rs(x)) dist(x)=0;
else dist(x)=dist(rs(x))+1;
return x;
}
pop:合并根节点的左右子树。
void pop(int x)
{
v(x)=-1;
f(ls(x))=ls(x);
f(rs(x))=rs(x);
f(x)=merge(ls(x),rs(x));
}
与相比,可以查询最小值,代码量少,但是要多维护一个,其他就没有什么优点了吧……
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本