进阶数据结构学习笔记

进阶数据结构学习笔记

来自\color{Gray}\texttt{SharpnessV}内卷省选复习计划中的进阶数据结构

不妨先看看前一篇awa

像上一篇一样,先列出用到的高级算法/数据结构/思想:

  • 线段树合并1,5,6
  • 可持久化2,3,4,7
  • 线段树上二分4,5,8
  • 线段树分裂6
  • 二分答案6
  • 贪心7
  • 8

下面是例题时间!

例题1

P4556 [Vani有约会]雨天的尾巴 /【模板】线段树合并

给你一棵树,每个节点都是有一个可重集合,每次选择一条链,向链上的每一个点的集合中都插入一个数。最后输出每个点的集合中出现次数最多的数,若有相同的输出小的。

我们将每一个修改xy树上差分后变成为1x,1y,1lca(x,y),即我们在x,y上各打上一个+1的标记,在lca(x,y)上打一个1,在falca(x,y)上也打上一个1即可。

这个标记我们使用动态开点的权值线段树维护。对每一个点都开一颗权值线段树,然后对于每次修改进行单点修改同时上传标记是维护最大值。

最后统计答案,从叶子节点向上合并线段树。线段树的合并很简单,就是将一棵线段树的有用的节点的数据合并即可。

Code

例题2

P3919 【模板】可持久化线段树 1(可持久化数组)

继续使用动态开点线段树,记第x次修改/询问的根节点为rootx

修改时,我们一般的线段树使用时是形如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);
	}
}

Code

例题3

P3402 可持久化并查集

可持久化并查集基于可持久化数组。

并查集的最根本的操作是查询一个点的父亲,即fx,可以看作是一个数组f的第i位。但是我们使用并查集是还需要用到一些优化比如按秩合并,路径压缩等。在路径压缩中,查询操作中的修改太多了,我们放弃这一种方式,而转念想一想,路径压缩是否可以用于可持久化并查集?当然可以。

在并查集上我们需要修改x的父亲fx(不是指树的根,是直接的父亲),只需要f[x]=y。但是为了可持久化我们就损耗一定的时间(换取每一次版本保存的时间),在[1,n]上的线段树进行可持久化数组那样的change(int &p,int pre,int l,int y,int pos)操作。查询f[x]也是如此(但是就不用修改了,不需要引用啥的)。

并查集还有一个操作是getf(int x),即得到x所在的树的树根。我们暴力向上跳fx(用上面的那种,每查询一次父亲的时间复杂度是O(logn)的),直到fx=x则返回x

merge操作是基于getfchange的,就不用说了。

ask操作就getf(x)==getf(y)即可。

注意数组要开大一点,一次修改操作的时间复杂度&空间复杂度都是O(logn)的,所以总时间复杂度就是O(mlogn)的。

再注意一点,change操作中,如果修改的左子树,就要把右子树直接copy过来,反之同理。所以我先copy,再进行下一层的change,反正下一层一定会新建节点的。

依照pre所在的旧版本,将p所在的新版本中的x的父亲设置成v.

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);
}

Code

例题4

P3834 【模板】可持久化线段树 2(主席树)

经典的问题——静态区间第k小。

我们对序列上每一个点x的前缀(记为prex)建立一棵动态开点权值线段树,然而发现相邻的两颗树之间只有微小的差距,有些节点是不变的。所以在建立prex时,我们就基于prex1,相同的节点直接复制,有修改的节点就新建节点。同时维护sump权值线段树上的节点p所表示的[l,r]可以中有多少个数字。

查询时,我们将询问(l,r)差分为l1r,然后就同时在两颗线段树上进行线段树上二分

详细地,假设当前节点是xy,其所代表的区间皆为[l,r]。那么区间[l,mid]中就有sum=sumls[x]sumls[y]个数字。假设现在是要求这个区间的第k小数,那么如若ksum,则第k小数在p的左子区间[l,mid]中;否则就在右子区间[mid+1,r]中。

我自己写了一个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);
}

Code

例题5

P3224 [HNOI2012]永无乡

有一堆点,每个点都有点权(保证没有两个点的点权是相同的,反正就是直接给排名啦)。每次有两个操作,一是将两个点之间连边,二是询问一个点所在的连通块中排名第k小的点的编号,若不存在则输出1

这两个操作感觉很模板:合并两个连通块,用并查集维护连通性,然后对于排名可以用线段树合并来维护;询问排名第k小,就类比P3834 【模板】可持久化线段树 2(主席树)中的线段树二分即可。

Code

例题6

P2824 [HEOI2016/TJOI2016]排序

给你一个序列a,有m次操作,每一次操作将区间[l,r]重新排序(0是升序,1是降序)。最后输出aq

n,m105,1qn.

看见只有一个询问,当然是从这里入手了。

考虑二分答案:假如我们已经知道了修改最后,aq=ans,那么我们可以二分这个ans不是嘛。如何checkans是否合法呢?

首先要知道一个trick:可以发现对0/1序列排序就相当于将所有0全部放到了1的左边,也就相当于先记录这个区间的sum,后将[l,rsum]覆盖为0,将[rsum+1,r]覆盖为1,这个可以用线段树在O(logn)的时间复杂度内完成一次修改。

如果我们已经知道了答案是ans,那么我们不在关心其他一对值之间的大小关系,只关心其他值与ans的相对关系。所以我们将小于ans的所有数全部变成0,大于等于ans的数全部变成1,然后看aq是否为1,是1的话说明ans可能偏大。否则ans一定偏小。

当然也有用线段树分裂&合并O(mlogn)的时间复杂度内处理多组询问的算法,这里继续留白awa

Code

例题7

P3293 [SCOI2016]美味

给你一个序列x,每一次询问

maxi=lr(a+xi)xorb

看到这个题面,不妨先想一想最简单的没有l,r,a,b这些参数我们可以暴力扫建立一棵0/1trie然后在trie上面贪心1的那一条边走,没有1边才走0边。

加了一个b怎么办?看看b的当前一位如果是0的话还是优先向1否则优先向0就是了。

有了[l,r]怎么办,拿主席树来维护吧!将两个线段树相减就可以得到一段区间中的数了。查询是否有1这一条边,发现在数值上相当于查询[now,now+(1<<i)1]区间中是否存在数,这个就可以用线段树查询了!

有了个a?每次查询的时候加上个a不就好了?

最后算出来的答案别忘记xorb.

Code

例题8

给你一个序列a,定义一段区间的价值为val[l,r]=i=lrai每次询问区间[l,r]中前k大的不同区间的价值之和。

1n5×105,1kminn(n1)2,2×105,0ai2321.

既然是要选择区间,那么我们将区间转换为前缀形式,即设si=val[1,i],则val[l,r]=srsl1

现在题目变成:给定一个si数组,且s0=0,求0i<jsisj的前k大种取值之和。

i<j可以变成ij,最后选择两倍的k并将和除以二即可。当i=jsi=sjsisj=0,一定不会成为答案,所以这个约束也可以去掉。

现在题目变成:给定一个si数组,且s0=0,求sisj的前2k大种取值之和的一半。

如果把s插入到01 trie中,那么没给定一个k,我们可以在O(logw)的时间复杂度内找到与一个数异或结果第k大的数。方法类似于线段树上二分,可参见例题4和例题5。

这时,我们用一个大根堆来计算答案。堆开始时放入sis中的元素异或的最大值(用01trie查询)同时保存信息irnk。每一次取出堆顶,假设堆顶是(x,i,rnk),把x加入答案,找到sis中的元素异或的第rnk+1大值变成新的x并将(x,i,rnk+1)放入堆中。进行2k次操作,那么取出的依次就是s中元素两两异或的前2k大值,再除以二即可。(我个人感觉这部分的思路来自一种sb单调性,同P2048 [NOI2010] 超级钢琴

时间复杂度O(n+k)logwlogn

注意trie的空间请尽可能的开大,越大越好,只要不MLE,就往死里开。

一般开上2030倍就差不多了

Code

例题9

P3690 【模板】Link Cut Tree (动态树)

LCT,一种基于splay的数据结构,用来动态处理树的形态和连通性问题。

Code

例题10

P3203 [HNOI2010]弹飞绵羊

很巧妙的将一个奇怪的问题转化成了树上问题!

Code

例题11

P3377 【模板】左偏树(可并堆)

支持两个操作:合并两个小根堆,查询第x个数所在的堆的堆顶并将其删除。

首先是并查集的找父亲:

int getf(int x)
{
	if(f(x)==x) return x;
	return f(x)=getf(f(x));
}

左偏树的合并很像FHQTreap,因为他们都是基于堆的,比较关键字的优先级。

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));
}

FHQTreap相比,可以O(1)查询最小值,代码量少,但是要多维护一个dist,其他就没有什么优点了吧……

Code

posted @   Vanilla_chan  阅读(953)  评论(0编辑  收藏  举报
编辑推荐:
· 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搭建本
点击右上角即可分享
微信分享提示