你对我来说还仅仅是『梦』|

Nityacke

园龄:3年粉丝:20关注:24

可持久化数据结构

前言

可持久化数据结构是一些很厉害的东西,大家可以去看 lxl 的课件和她在 WC2024 上的讲课,由于能力限制,这个博客会讲得比较基础。

根据 lxl 的课件,我们可以把其分为几类:

部分可持久化:允许访问历史版本,不能修改历史版本

完全可持久化:允许把目前版本替换为历史版本

Confluent persistent:允许合并历史版本,几乎不太能维护

这个 Confluent persistent 是什么逆天东西我也不知道,一般不会出现。

然后对于可持久化的维护,一般目前 OI 里的维护方法都是 path copy 的方法。

还有一种叫做 Fat Node 的方式,我们对维护的每个元素维护一个二元组的 vector 表示 (时间,权值),查询的时候二分出 T 时刻的权值即可,但这个东西没看出什么优越性。

以及一种叫做 Node-splitting 的方式,这个东西可以常数代价可持久化任意一棵度数为常数的树,支持维护父亲,具体的实现可以去看 lxl WC2024 的课件。

以上全是没用的瞎扯,我们来讲点有用的东西

如果允许离线。

对于部分可持久化我们可以直接对历史版本扫描线,然后就不需要可持久化数据结构。

对于完全可持久化,我们可以建出操作树,然后在树上 dfs,需要支持修改和撤销,不能用均摊数据结构。

以下问题没有特殊标注 n,q 都默认为 105

可持久化线段树

不想区分可持久化线段树,可持久化权值线段树,主席树等神秘名字,以下称呼可能比较随意

P3834

多次询问区间第 k 小值。

我们首先考虑把这个问题扔到二维平面上,我们不妨设第 i 个点为 (i,ai),那么我们就是询问 lxr 中的点里,yk 小的点的大小。

我们考虑我们对 x 扫描线,同时对 y 建出主席树,此时我们需要的只是一个部分可持久化。

我们不妨考虑对于 l=1 的情况如何解决询问,我们找到第 r 个版本,在上面二分值在前一半的数的个数是否到达了 k,这个可以线段树二分。

我们考虑一般的情况,我们二分的时候只需要值域在某个区间的数的个数,我们发现这个是可以差分的信息,于是我们在 l1 个版本和 r 个版本上同时二分即可。

P9175

首先我们知道答案上界:这条路径上的 si 最小值,这里可以随便用个什么东西维护。

然后我们知道,询问的费用是可以差分的,我们在树上建出前缀的主席树,那么询问就可以转化在 O(1) 棵主席树上统计。

那么我们目前的知道每颗主席树维护了若干二元组,我们不妨设主席树维护为 (速度,费用),每个边就相当于一个 (vi+1,wi) 的点。

那么我们需要询问费用和的权值不大于 ei 的情况下,不能包括的点的速度的最小值,这个我们可以在主席树上二分维护。

P2048

我们考虑对每个点 i 维护用其作为右端点的贡献最大值以及取到最大值的左端点 posi,然后我们把所有右端点的五元组 (L,R,posi,i,val) 扔进堆里,每次取出 val 最大的一个,然后考虑把右端点是 i,左端点为 [L,posi)(posi,R] 的两种情况分别扔到堆里即可。

寻找最值的方法可以用线段树维护区间最大值。

BZOJ3784

我们先把 kk×2,然后我们就可以不用管 uvvu 是同一条道路。

然后我们考虑类上面的方式,考虑对于每个点,怎么找到距离他最远的点,我们考虑先建出对于 1 每个点的距离的线段树,然后在上面 dfs,每次进入一个子树的时候,相当于一个子树的距离减,剩下的部分加,然后主席树维护即可。

P2839

给定一个序列,每次询问给定 a,b,c,d,询问 l[a,b],r[c,d] 中 中位数最大的区间 [l,r]

询问满足 a<b<c<d

强制在线。

首先我们有需要知道一件事,如果中位数 v,我们把 v 的数看成 1<v 的数看成 1,那么这个区间的和 0

我们不妨考虑暴力怎么做,我们枚举 v,然后计算 [b,c] 的和,[a,b) 最大后缀,(c,d] 最大前缀,判断三者的和是否 0 即可。

我们发现有意义的 v 只有 O(n) 个,然后我们发现计算的一个瓶颈在于我们统计三者的和,我们可以对于每种情况建出线段树,然后就可以 O(logn) 的判断一个值 v 是否合法。

由于建不下 O(n) 棵线段树,但是每次 v 增加到下一个数的时候都只有一些位置的权值从 1 变成 1,而改变的总次数是 O(n) 的,我们可以用主席树,就解决了这个问题。

很明显,v 是可以二分的,我们用主席树和二分就可以在 O(nlogn+mlog2n) 的复杂度内解决问题。

P7357

就是 P2839 放到了树上,且带上了单点异或 1 的操作,我们可以在离散化需要的 v 时将 aixor1 也离散化进去。

我们对每个点维护其在中位数为 v 时候的到根的权值和,那么我们每次二分询问就相当于子树最大值。

然后我们发现,对于一个 ai 异或 1 的操作,只会影响 v=2ai2+1 这一个值,而影响相当于子树加。

那么拍到 dfn 序上,问题变成区间加区间最大值,时间复杂度 O(nlogn+mlog2n)

P2633

P3834 放在树上,我们考虑树上前缀和,那么 uv 路径信息就可以转化成在 O(1) 棵主席树上查询。

P3302

如果这个题不强制在线,我们可以离线将树结构建出来,然后用 P2633 的做法启动,但是现在强制在线了,就不大行。

我们如何合并两棵树,我们可以暴力 dfs 其中一棵树,然后更新每个节点的前缀信息,复杂度是 O(szlogn) 的。

我们发现这个复杂度只跟被访问的树有关,我们考虑启发式合并,则每个节点只会被访问 O(logn) 次,总时间复杂度 O(nlog2n)

P4602

我们发现要最小值尽量大,考虑对于每个询问怎么做。

先二分,然后我们就能在美味度 mid 里面选择果汁,我们贪心的选择单价最小的,最后判断体积是否大于 L 就行了。

我们发现这个可以主席树优化,主席树下标表示单价,然后维护区间费用和以及区间有多少升饮料即可。

询问时外层二分,然后主席树上询问,时间复杂度 O(nlogn+mlog2n)

P3168

我们发现每个任务相当于在 si 处插入一个集合,ei+1 处删除,然后每次就询问某个时间的集合前 k 大。

我们发现一共的插入删除次数是 O(n) 的,主席树维护有哪些值即可。

P3755

静态二维数点板子题。

P7424

子弹能爆了多少木板不好计算,我们考虑计算每个木板会被哪个子弹爆了,很明显,这个东西可以二分。

那么我们每次就要计算,[1,mid] 的子弹中,有多少个元素位于 [L,R] 中,这是一个静态二维数点问题,可以 O(logn) 做。

所以总时间复杂度 O(nlogn+mlog2n)

以及还有 O((n+m)logn),做法,你对每个下标前缀维护子弹爆炸的时间,然后一次询问就相当于区间第 k 大,即可 O((n+m)logn)

P7834

我们考虑建出 Kruskal 重构树,那么不经过长度超过 x 的边所能到达的点集是一棵子树,而这棵子树可以倍增找到。

那么问题转化成子树第 k 大,然后我们拍到 dfn 序上就是区间第 k 大,时间复杂度 O((n+m)logn)

P4587

我们不妨去掉区间限制怎么做。

我们不妨设序列已经从小到达排好序,我们目前能表示出 [1,S] 中的所有数,那么对于一个还未使用的数 ai

  • ai>S+1,这种情况下 S+1 一定不能表示出来,答案就是 S+1
  • 否则我们可以将 ai 加入构造方案中,SS+ai

而且由这个过程可知,S 一定是 a 的一段前缀和。

我们可以用主席树来优化这个过程,首先我们查询 S+1 的数的和,设其为 v 如果 v 等于 S,说明答案就是 S+1

否则 Sv,继续操作。

我们考虑这个过程会被操作多少次,首先一次 Sv 操作后,我们不妨设 vv 经过操作后得到的数且 vv,不然就已经结束,那么由于 vv,所以存在 ai(S+1,v+1]v>2S,可以知道这个过程只会进行 O(logV) 次,总时间复杂度 O(nlogn+mlogVlogn)

P6071

对于这种问题,我们不妨分讨一下:

lca(alr)=x

  • lca(x)subtree(p),答案为 disxdisp
  • alr 有一部分在 p 子树内,答案为 0

对于剩下的情况,我们也分讨以下:

  • 路径的并是一条先上后下的路径,此时 p 不在 x 子树内,距离为 disx+disp2dislca(x,p)
  • 否则相当于将 p 加入 [l,r] 的虚树后 p 到其父亲的距离,根据经典结论,我们只需要在主席树上查询 dfn 的前驱后继即可。

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

CF1416D

这个题有简单的重构树做法,但是我们考虑一些其他的做法。

我们考虑时光倒流,并可持久化线段树合并,那么现在最唐的问题是我们需要删除节点,这个东西感觉很迷惑。

那怎么办捏

我们发现这个 p 互不相同,这说明了什么,说明最底层的叶子大家都是相同的节点!

我们就可以在线段树上暴力寻找,如果一个子树里面都没有数或者都被访问过,那么我们就可以给他打上标记,然后下次就可以直接返回。

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

P7172

我们考虑把度数始终为二的链缩起来,则原树的规模就能降到 O(n),除了 x,y 满足直接为祖孙关系的节点之外,答案只会是每条链的末尾,求解 O(n) 个点的 lca 问题是简单的,那么问题转化成了知道 x,y 分别在那条链上,我们从下往上一层层地考虑,发现每层只会删除 O(1) 条链,并加入 O(1) 条新的链,然后知道 x,y 在每层的第几个是简单的,那么主席树需要维护的就是单点修改,删除,询问第 k 个数,这是简单的。

P7577

prei 为最大的满足 aprei=aiprei<i 的数。

我们发现一个 G 函数里面的东西是连续的,具体来说

G(F(L,c),F(L,c+1),F(L,d))=1+i=c+1d[prei<L]

也就是在 [c+1,d] 中出现,却没在 [L,c] 中出现的数的个数。

而且我们可以发现,这个 Gc,d 不变的情况下,随着 L 的增大而增大,我们可以二分合法区间的左右端点,这时我们已经能够得到 O(nlog2n) 的二分套主席树做法。

但是我们还可以做到更优,我们将每个点看成 (i,prei) 的一个点,然后我们发现就需要找到最小的 l 使得 x[c+1,d],yl 的点的和不小于 e,最大的 r 使得 x[c+1,d],yr 的点权和不大于 f,这个可以主席树上二分做到。

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

P4846

我们先考虑对于整个序列,且没有 k 的时候怎么做,我们将序列差分,然后转化成我们每次一次操作可以单点 +1,并让另一个人 1,那么最后的答案就是 |bi|/2

然后我们考虑有这个加上 k 的限制,那么其实就是差分数组里面若干个数可以变成 k 或者 k,等价于我们可以先将一个数加上 k 或者 k

然后我们考虑最后的 b 都要为 0,所以加上 k 和加上 k 的数的数量相同。

对于一个原来是 a 的数,变化后的贡献是 ka(a)=k2a

对于原来是 a 的数,变化后的贡献也是 k2a

那么我们假设有 s 对数变化,那么我们对答案的贡献是 ks2val+2valval+val 是绝对值前 s 大的正数/负数的绝对值之和。

如果我们知道 s,那么我们是可以 O(logV) 的代价用主席树求出最小花费的,所以现在的问题变成了求 s

容易发现,贡献关于 s 是一个凸函数,那么我们可以通过二分来找到最优的 s

时间复杂度 O(nlogV+mlognlogV)

P7518

我们先对节点权值重新标号,对于询问 (x,y),z=lca(x,y),我们可以将其拆成 xzzy 的两条链,首先我们对于每个数,维护 f1x,i 表示跳到权值为 ax+2i 的点是哪个,f2x,i 表示跳到权值为 ax2i 的点是哪个,那么 xz 的贡献是好做的。

假设 xz 答案为S,那么怎么计算 zy 贡献呢,从上到下不好计算,我们考虑从下到上,先二分一个 mid,然后用主席树找到 y 的第一个权值为 mid 的祖先,然后倍增跳到权值为 S+1 的点,与 z 判断深度关系即可。

时间复杂度 O(nlogn+mlog2n)

P6794

这个题我们看看需要支持啥:

  • 对于 1 操作,我们先判断单点水位,然后相当于找到左右第一个隔板高度大于等于 h 的位置,区间赋值,这个可以线段树上二分维护。
  • 对于 2 操作,相当于 i<x,ai=min(ai,maxj[i,x)hj),对于 i>x 情况类似,所以我们考虑对每个节点,维护区间隔板最大值,然后下传标记同时将右边最大值往左边传即可。
  • 对于 3 操作,我们直接修改隔板高度。
  • 对于 4 操作,我们直接询问即可。

那么我们可以将问题化为:

  • 找到左右第一个 h 的隔板。

  • 区间赋值。

  • 区间用类单调栈的方式取 max

  • 隔板单点修改。

  • 单点查询。

我们对于线段树上每个节点,考虑维护其最大值,以及三种标记。

然后 push_down 时,标记的下传是容易的。

对于可持久化,这个题只是一个强行嵌套,难度不大。

P8147

对于这个问题,我们先考虑先对 si 建出 AC 自动机,然后我们每次将 S 放到 AC 自动机上匹配,那么问题变成了有 O(|S|) 次点到根 +1 的操作,询问每个点的 ci 乘上其覆盖次数的第 k 大值。

我们不妨用一次 dfs 求出所有节点的覆盖次数,然后考虑二分,那么我们对于每个节点,就发现我们只需要判断这个节点有多少个值小于某个数,这个我们可以对每个节点维护动态开点线段树。

这个很唐的问题在于我们每次枚举每个点,而点的数量可能是 O(|S|2) 的,比较难绷。

但是我们发现,如果将 S 在 AC 自动机上匹配的状态代表的点找出来建出虚树,那么这颗虚树的点数是 O(|S|) 的,且每条虚树边被覆盖次数相同。

那么我们每次二分之后问题变成了询问 O(|S|) 条链上小于某个值 v 的个数,每条链询问的 v 不一定相同,这个我们可以在树上建主席树解决,那么这个问题就能做到 O(|S|logvlogans)

我们发现修改查询不平衡,修改有 O(n) 次,查询有 O(|S|logans) 次,你可以使用一些神秘技巧平衡一下。以下内容不保真

数据结构维护范围是 O(v),然后每层分 x=vlogv|S|logansnv0.72=144 块,大概能做到 O(|S|loganslogv|S|logansn)

P7712

首先对于这个题,我们考虑优化建图,那么我们比如对横着的线段按 y 扫描线,那么就是在每个竖着的线段在一段时间内单点存在,我们相当于插入删除,但是由于建图连边是不容易删除的,我们可以新开一个版本,然后在新版本上修改后连边。

现在的问题是我们直接做是不行的,因为虚点可能改变原图的双连通性,我们考虑不建出虚点,考虑 Tarjan 算法流程:

  • 遍历所有未遍历的点,这个我们可以主席树上每个节点维护这个区间内有多少个点未被经过,这里有个性质,就是对于每条线段,其在主席树上的叶子对应的都是同一个,所以不会出现一个点跑多遍的情况。
  • 找到所有能到达的点的 dfn 最小值,由于对于每个点,他能到达的点是一个版本的一段区间,这个可以直接询问即可。

然后这个题就可以做到 O((n+m)logn)

CF1148H

首先,我们考虑按右端点扫描线,由于要支持右端插入,这跟我们扫描线方向相同,这个就很萌萌了,可以解决插入的问题。

然后我们考虑对于一个右端点,每个 mex 值对应的一定是一段区间,且 mex 关于 l 单调。

然后我们比如插入一个值 v 时,我们会对 mex 为 v 的区间产生贡献,然后会将这个颜色段分裂成若干颜色段,由于颜色段均摊的性质,我们一共只会产生 O(n) 个颜色段。

那么对于每一个颜色段,我们就是区间 [l,r][L,R] 时刻会造成 1 的贡献, [l,r] 是左端点的范围,[L,R] 是右端点的区间,然后一次询问就是某种颜色 [l,r,l,r] 的权值和。

我们可以主席树维护。

具体来说,我们每个位置的贡献是一个一次函数,同时用 set 维护每个颜色的每个版本,细节比较多。

可持久化 Trie

这个东西我感觉跟可持久化线段树应该是本质相同的,但是可能只是结构不同。

P4735

先把异或做一个前缀和,然后转化成在一段区间中取一个点异或值最大,这个就是板子了

P4592

对 dfn 序建可持久化 Trie,对树上前缀和也建一个,然后一次询问就可以在 O(1) 个 Trie 上二分即可。

P5283

我们考虑把 P2048 的套路搬过来,那么这个就是板子了。

P5795

我们发现 n 不大,我们考虑暴力多树二分。

我们对长度为 m 的序列建出可持久化 Trie,那么每次询问我们在 O(n) 个Trie 上判断这一位能否取 1 即可。

LOJ6144

我们考虑只有异或怎么做,我们记录一个全局异或标记,然后再可持久化 Trie 上二分判断即可。

现在加入了与和或操作,我们考虑操作为与而且该位为 1 的时候没有影响,但是该位为 0 的时候,相当于所有该位作为右儿子的节点都去了左儿子,然后接下来的与/或操作就等价于异或了,所以我们在这种情况对整个序列暴力重构。

时间复杂度 O(nlog2V+mlogV)

可持久化并查集

P3402

我们用主席树维护数组,然后对于并查集部分按秩合并即可。

由于我们因为路径压缩的复杂度是均摊的,所以不能路径压缩。

但是我也不知道这两能不能同时使用

P4768

我们考虑先跑出 1 到每个节点的最短路。

然后我们将海拔从大到小扫描,然后相当于我们不断加边,维护一个点能到达的最小元素。

首先,对于一个询问,目前我们的状态是怎样的我们可以二分求出,然后我们只需要这个状态里面 x 所在集合最小值。

那么我们相当于合并两个集合,支持访问历史信息,我们可以使用可持久化并查集。

可持久化平衡树

一般的可持久化平衡树有 FHQ-Treap,WBLT,AVL 之类的,但是一般 OI 只有 FHQ 和 WBLT 能用,笔者只会 FHQ-Treap。

但是 FHQ-Treap 在做区间复制时因为会复制随机种子,所以会出一些问题,所以我们合并两个节点 a,b 时可以用 szasza+szb 的概率让 a 成为根,这个东西复杂度不知道是否正确,但是卡不掉就是了

upd:好像复杂度错误,而且 zx2003 会卡这个,这下这下了。

FHQ 的复杂度是比 WBLT 更劣的,前者合并复杂度是 log(siza+sizb),后者是 logsizasizb,然后就会在某些下头时刻拉出一个 log 的差距,比如在区间复制 k 次的时候,可以倍增预处理出 20,21,22, 然后合并,时间复杂度是单 log 的。

而且由于 FHQ 信息合并的常数更大,导致在某些题上 FHQ 需要大力卡常。

但是如果平衡树全是区间复制就会出一些问题。

这样的操作有很大的局限性,因为我们平衡树的复杂度是 O(logn),而一次这样的操作就会使得n 扩大一倍,所以到最后 n 会变成指数级大小,导致平衡树复杂度退化为 O(n),这里还不考虑高精度造成的影响。

所以一般会限定序列长度。

当然我们在很多时候可以优化内存。

比如每 O(nlogn) 次操作重构整个序列,空间复杂度为 O(n+nlogn×logn)=O(n),时间复杂度为 O(mlogn+mnlogn×n)=O(mlogn)

P5055

板子。

P5586

区间复制的板子。

P8264

我们考虑没有区间限制怎么办,我们对于维护 f(x) 这个函数表示 x 经过变换会变成什么,然后我们从后往前考虑,每次遇到一个 v,设目前的为 f(x),而会变成 f(x),那么 xv,f(x)=f(xv),x<v,f(x)=f(vx),然后就是一段区间复制,平移,翻转,可持久化平衡树维护。

然后对于加上了区间限制,我们用 P7220 的套路,对每个询问,把其拆到 O(logn) 个区间上,每次处理后放到下个区间即可,时间复杂度 O((n+m)lognlogv)

P8263

我们先考虑重复 k 次怎么做,上面已经给出了倍增的解决方法。

对于操作 2,我们可以在倍增时预处理出 2i 反过来的节点即可。

然后删除和询问是简单的,使用 WBLT 可以一个 log 解决。

后记

可持久化数据结构还有很多,但是很多都很套路,在这里也不能一一讲述,大家如果觉得以上的内容不够的话,还可以去学习可持久化可并堆,可持久化边分树等内容。

本文作者:Nityacke

本文链接:https://www.cnblogs.com/Nityacke/p/18090908

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   Nityacke  阅读(113)  评论(2编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起