数据结构学习笔记

势能均摊

对于一些难以维护的区间操作,我们可以考虑一个数被操作的次数,对于某些变换,可能操作次数非常小,所以我们只进行有用的操作,复杂度仍然是正确的。

例如区间开根,如果值域是 1018 最多操作 7 次就会变成 1,接下来就不会变化,所以直接做就可以了。

这种问题的维护方式通常有两种:一种是使用并查集,如果一个数 i 不需要再操作了就把 ii+1 合并,这样的复杂度是 Tnα(n) 的,其中 T 代表最大操作次数。

上面这种东西的局限性在于难以快速得到区间的某些信息,当然可以上个数据结构,但是这样就不如在线段树上直接做,到叶节点后暴力修改。但是有的区间因为有一个数还能动就被遍历到了,造成了一定的浪费,但一个数最多浪费 logn 个区间,所以复杂度仍为可以接受的 Tnlogn

那么什么操作有这种性质呢?常见的有 gcd,mod,popcount 等,这些东西每次改变都至少会变成原来的 12,所以操作数在 logV 级别。

甚至或操作这种也是可以的,因为每次操作只会把原来是 0 的位变成 1,所以最多也只会有效操作 logV 次。

例题:

P9989 [Ynoi Easy Round 2023] TEST_69

gcd 最多减少 log 次,所以用势能均摊线段树即可。

对于 [l,r],我们要操作当且仅当存在至少一个 p[l,r]gcd(ap,x)<ap

正难则反,一个区间不用操作首先要要求 xmaxap,并且所有 gcd(ap,x)=ap(显然不会大于),那么 x 是所有 ap 的倍数,那么也就是 xlcmlirai 的倍数。

注意到 x1018,所以 lcm>1018 就不用管,一定要操作。

线段树合并

例题:

P3224 [HNOI2012] 永无乡

简单题,直接用动态开点权值线段树维护每个点目前的情况,并查集维护连通性。合并就是并查集的两个根合并即可。

因为查询时只用到并查集的根,这个根必然没被合并,所以每次这个根上的信息都是对的。

是排列,所以直接记下 pos 就可以得到编号了。

Submission

P3899 [湖南集训] 更为厉害

首先 a,b 都比 c 更厉害,所以 a,b 都是 c 的祖先。

那这样事情就很好描述了,ab 只需满足 |depadepb|k 即可。

又知道 a 是哪个点,所以 b 的深度范围就是 [depak,depa+k]

对于每个 (a,b)c 只要在它们较深的子树中即可。

depb<depa 时,这样的 b 一共有 min(depa1,k) 个,每个对应一个 Subtree(a),贡献为 min(depa1,k)×(siza1)

depb>depa 时,对于每个 b,贡献为 sizb1。那么我们可以用哪些 b 呢,在 a 的子树里即可且 depbdepa+k 即可。线段树合并,每棵线段树以 dep 为下标,Tx 表示 x 及其子树的线段树,每次查询 Ta[depa+1,depa+k] 的贡献之和即可。

一个需要注意的点是如果你直接把两棵树合并的话树中的信息会改变,但我们是进行完所有合并后再查询,所以说这样会导致答案的错误,解决方案就是每次合并也新建节点,只是这样节点数会翻倍。

Submission

接下来就是比较牛逼的东西了

例题:

P5298 [PKUWC2018] Minimax

首先将权值离散化,然后概率乘上 (110000)1

考虑 dp,设 fx,i 表示节点 x 的权值为 i 的概率。

显然,将权值排序后答案就是 i=1mivif1,i

对于 fx,i,考虑以下几种情况。

x 没有儿子,那么 fx,vx=1,其他均为 0

x 仅有一个儿子 son,那么 fx,i=fson,i

x 有两个儿子 ls,rs

分别考虑是取最大值还是最小值,以及取得是哪个数,可以得到:

fx,i=px(fls,ij=1i1frs,j+frs,ij=1i1fls,j)+(1px)(fls,ij=i+1mfrs,j+frs,ij=i+1mfls,j)

考虑用线段树合并维护,那么左右两棵树分别要乘上一些不同的东西,计算即可。

然后这里的点在于我们不在注意线段树代表的区间,而只关心两个节点的存在情况合并,这样并不会改变复杂度,到了一个空节点对另一个节点打上 tag 即可,然后这样最后一定是走到了一个空节点才会停,这样就已经把 ls,rs 两棵树树上的信息合起来了,那么这个时候是对的,往上走的过程也是对的。

线段树每次走区间的时候,就恰好帮我们维护了前后缀和,同时因为权值互不相同,若一边的 px 有值那么另外一边一定是 0,所以直接使用左右两边的区间和即可。

Submission

KTT

例题:

The Third Grace

首先考虑 dp。设 fi 表示最后选了 i,但未考虑 i 的贡献时的答案。

初值可以直接令 f1=0。为了算上终点的贡献,可以添加一个点 m+1,则最终答案为 fm+1

转移就考虑上一个点选的是 j,计算 j 的贡献,即 j 是区间中最靠右的位置的数量与 pj 的乘积。

考虑这样对区间 [l,r] 有什么限制,首先 [l,r] 肯定包含 j,同时因为 ji 的左边,但是是 [l,r] 中最右的点,所以 r<i

所以得到转移方程为 fi=max1j<ifj+pjk=1n[lijri<i]

这样是很难优化的。考虑换一个角度,转为计算每个 ji 的贡献 gj,及前面那一坨式子的值,在 i+1i 时动态更新 gi。则 fi=maxgj

那么考虑从 ii+1 时发生了什么变化。与 i 有关的只有可用的区间,且有贡献的区间是随着 i 的增长逐步增加的,所以每次即将从 ii+1 更新时,将 rj=i 的线段 j 加入可用的集合。同时更新即可。更新时只需对于新加入的一个区间 [l,r],满足 ljrg 都可以加上 pj,即这个新区间的贡献。

那么最终每个 gi 都可以表示为 fi+xipi 的形式,考虑用数据机构维护之。需要支持的操作有以下三种:

  • 对于区间 lirxi+1xi

  • 单点修改一个点的 fi,pi

  • 查询区间(其实也可以全局)最大值。

考虑使用 KTT,初始时可以将 fi,pi 全部赋为 。最后查询全局最大值即可。

注意 KTT 的 b 相同时优先选 k 大的,时间复杂度 O(mlog3m)

Submission

树套树

例题:

CF1093E Intersection of Permutations

出现了两个排列的对应,考虑算出每个 bia 中对应的位置,记为 pi

那么现在我们要做的就是:

  • 查询 i[lb,rb],pi[la,ra]i 的个数。
  • 交换 pi,pj

外层树状数组,内层动态开点权值线段树维护即可。交换操作相当于对某个位置某个值的加减。

注意内存回收,对于某个节点,如果它维护的权值数量变成了 0,那么把它删掉即可。

因为每个时刻每个 i 只对应一个 pi,所以空间复杂度就可以接受了。

Submission

P3759 [TJOI2017] 不勤劳的图书管理员

题意是给你序列 a,v,每次交换 ai,ajvi,vj,动态维护 i=1nj=i+1n[ai>aj](vi+vj)

首先 i=j 是没有意义的操作,可以直接跳过。

考虑交换产生的影响,这里假设 i<j,显然只有在 (i,j) 间的数会发生变化。

我们令 L=min(ai,aj),R=max(ai,aj)za(i,j) 中的某个元素,wvz 的位置上对应的值。

  • z[L,R],考虑原先 ai,aj 的大小关系。

ai<aj,那么原先 zai,aj 均不构成逆序关系,交换后构成了两组逆序关系,答案增加了 vi+vj+2w
ai>aj,那么已经构成的逆序关系被消除,答案减少 vi+vj+2w
ai=aj,显然没有影响。

  • z[1,L),原先 z 与左侧的 ai 构成了逆序关系,交换后与 aj 构成了逆序关系,答案由 vi+w 变为了 vj+w

  • z(R,n],原先 z 与右侧的 aj 构成了逆序关系,交换后与 ai 构成了逆序关系,答案由 vj+w 变为了 vi+w

最后再更新一下 aiaj 构成的逆序关系造成的影响即可。

为了快速维护以上操作,我们需要查询某个下标区间中 a 的值在某个值域区间中的数的数量和这些数的和,开两颗树状数组套权值线段树即可。

写的时候注意区分 av,空间一定要开够,至少要开 128 倍。

Submission

平衡树

例题:

CF85D Sum of Medians

考虑平衡树,记 Sx,j 表示 x 的子树中模 5j 的位置的答案,那么问题只在于 pushup 里,此时仅需考虑 x 的子树。

我们都知道,一棵平衡树的中序遍历就是它的关键字顺序,所以当前 x 的值应该排在 sizlsx+1 位。

那么可以得到,Sx,j=Slsx,j+Srsx,(jsizlsx1)mod5+[j=(sizlsx+1)mod5]wx

Submission

P4008 [NOI2003] 文本编辑器

写一个按照大小分裂的 FHQ treap 就好了。可以记当前的指针 now 表示 now 前面的数的数量,就跟题目描述中的东西完全对上了。

Submission

LCT 的核心内容在于 access 操作,其打通一条实链的操作使其 LCT 有着灵活多变的结构和极其强大的功能。

一些应用:

维护生成树

维护子树信息

考虑一个点有的儿子:除了一个实儿子,还有若干个虚儿子,那么我们要同时维护实儿子和虚儿子的信息,实儿子是好做的,splay 可以帮我们维护,那么虚儿子怎么做呢?

例题:

[ABC350G] Mediator

考虑 LCT,首先判断 u,v 是否在一个连通块内,如果不是答案就是 0。否则将 u,v 间的路径提取出来。如果维护这条路径的 Splay 大小不为 3 答案也是 0。否则,答案就是这棵 Splay 中深度排中间的点。

Submission

主席树

例题:

CF226E Noble Knight's Path

先树剖,然后考虑对操作的时间建主席树,那么修改直接操作即可,考虑查询。

这里,我们对一条重链上的可以使用的城堡计数有两条限制,主席树可以帮我们解决掉时间的限制,但是还有区间的限制怎么办呢。

一开始应该会想到像区间第 k 大一样的二分操作,但是这样是不对的,因为我们要查的是一段区间,主席树维护的是一段时间,而对于点,它维护的是 [1,n] 这个整体。所以不能直接在两棵树上做差分。

那么考虑先在主席树上做一次区间查询,然后对查询到的叶子依次通过差分+二分找第 k 个,这样点都对应到了查询区间的子区间。

注意 u,v 不计,以及计算时的顺序,正反很重要。

Submission

cdq 分治

例题:

P4169 [Violet] 天使玩偶/SJY摆棋子

考虑没有修改怎么做。

那么就是,给你 n 个点,m 次询问,每次给定一组 x,y,求 min1in{|xxi|+|yyi|}

分类讨论:

  • xix,yiy,求 x+y+min{xiyi}

  • xix,yiy,求 xy+min{yixi}

  • xix,yiy,求 yx+min{xiyi}

  • xix,yiy,求 xy+min{xi+yi}

把所有的询问和修改按照 x 排序,那么每次就是一段前 / 后缀最小值的查询,用树状数组即可。

现在加上修改。

使用 cdq 分治,设计函数 solve(l,r) 表示操作序列第 lr 项的计算。

先分治两边,然后问题就在计算 [l,mid][mid+1,r] 的贡献。

把左右两部分的点按照 x 排序,按照上文做法,最后清空树状数组即可。

时间复杂度 T(n)=2T(n/2)+O(nlogn)=O(nlog2n)

Submission

[BalkanOI2007] Mokia 摩基亚

同样考虑不带修改。

那么可以把一个询问拆成四个,即 w(x2,y2)w(x11,y2)w(x2,y11)+w(x11,y11)w(a,b) 表示 x[1,a],y[1,b] 的矩阵。然后按 x 坐标排序,依次往里面加点,用树状数组查询,最后拼起来即可。

带上修改,就使用 cdq 分治即可。

Submission

离线扫描线技巧

例题:

P3863 序列

考虑 n=1 时的做法,那么就是查询一段区间内 x 的数的个数,十分的经典。

现在问题变为了区间操作,把区间操作用差分拆成 lr+1 的操作,这样区间操作就转化为了单点操作。我们将操作离线下来,从小到大考虑每个 1ini,计算在 i 上的所有修改,如果是 l 处的操作就在 [nowt,m] 上加上修改的数,否则减去修改的数即可。注意这里是时间轴上的区间操作,原理是对于 i 这个数,每个会影响到它的区间修改在进行后就会一直存在,换句话说,假如 i 在某个操作中增加了 x,那么它从这个操作开始一直都有这个 x 的增量。我们通过扫描线的方式计算出了恰好所有会影响到 i 的修改,于是就正确维护了需要的信息。

对于一个查询 (p,y,t),直接查询在 [0,t1]yap 的数即可。

更通俗一点的说法是:我们就是维护时间轴,然后从 1n 依次考虑每个 i,计算从 i1i 时发生了哪些变化,这些变化都在 i 上,更新掉它们即可。

那么我们需要一个数据结构,支持区间加,查询 x 的数量,上个分块,散块暴力,整块保持有序后二分即可。设块长为 B,时间复杂度就是 O(m(mB+BlogB+B+mBlogB))

Submission

[SCOI2015] 情报传递

考虑一个点被标记的时间 tx,那么一个在时间 t 的询问就是问 u,v 间有多少 x 满足 ttx>c,转化以后就是 txtc1

把修改拉出来,询问按 tc1 从小到大排序,每次就是单点加,链求和,做完了。时间复杂度 O(nlog2n)。可以转成树上差分后子树求和做到一个 log

笛卡尔树

【梦熊 2025 炼石计划】 C.昔在、今在、永在的剧目

赛时想的是对于一个 OR 和超过 k 的区间,它肯定是越短越优,但是还要考虑长度 k,就很麻烦,但可能也能做,待补。

考虑另一种贪心:对于最大值不变的区间,肯定是越长越优,这样就不用考虑 k 了。这样的区间共有 O(n) 个,恰好对应着 a 的笛卡尔树上的点。

以下的合法均只考虑 OR 的限制。

接下来考虑修改只会让一些不合法的区间变成合法的,因为 OR 不会变小。

同时,对于大区间分出的小区间,如果小区间合法,那么大区间一定也合法。所以某一时刻合法的区间在笛卡尔树上构成了一个包含根的连通块或不存在合法的区间。进行修改只会让连通块往下面扩展。

一次修改会影响到哪些点呢?考虑笛卡尔树上代表的区间包含 i 的点是 i 到根的路径,所以只需要算这一块。找到第一个不合法的点,向下更新。向下更新的方向可以由笛卡尔树存储的区间计算。更新到不合法的点就结束,每次至多多计算一次,时间复杂度是对的哦。至于找到第一个不合法的点,使用高贵的倍增即可,因为一条链必然是 11100 的形式。

我们需要一些数据结构,检验区件是否合法用 SGT,查询后缀 min 用 BIT。

时间复杂度 O((n+q)logn)

posted @   aCssen  阅读(15)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律
点击右上角即可分享
微信分享提示