可持久化线段树/主席树 总结
前言
可持久化数据结构其大多数有两种用法,但是其本质都是维护了一个“前缀数据结构”。
第一种用法是对于可以差分的信息(不只是序列上的差分,也可以是树上差分),我们就可以直接通过“前缀和”相减来得到对应区间的信息,从而维护。
第二种用法比较纯粹,就是在我们需要访问这个数据结构每一个历史版本的时候,我们能够直接取出来使用了。
另外,主席树这个名称特指“可持久化值域线段树”,之后就不再提了。
正文
基础题
最常规的题目,大多是模板。
例题
BSOJ5526&LOJ519 数学上来先打表
操作树+值域分块+并查集,然后就比较容易了。
具体不再赘述,请看题解。
BSOJ6733【模板】可持久化数组
模板题,使用可持久化线段树维护即可。
BSOJ6735【模板】可持久化栈
似乎卡了空间,于是直接操作树建出来做。
BSOJ6734 【模板】可持久化并查集
可持久化线段树维护 \(fa\) 数组,并查集按秩合并即可。
BSOJ6729 【模板】区间第k小值(主席树)
模板题,做法思路就是主席树维护每一个前缀下标构成的线段树,然后发现每个数的个数可以差分,于是可以差分来实现区间询问,然后剩下的就是线段树上二分了。
SP11470 TTM - To the moon
可持久化线段树区间修改模板。
注意可持久化线段树尽量还是需要标记永久化,因为如果是标记下传的话,每次下传的时候还要多复制一下所有的标记,空间消耗极大,所以一般使用标记永久化,所以注意写法。
P1903 [国家集训队]数颜色 / 维护队列
讲过一万次的题目,区间数颜色问题。
我们之前使用的是离线后树状数组的做法,这里介绍在线的主席树。
还是对于每一个点维护前驱的位置,然后询问就是在两个树上差分,询问哪些下标的权值在区间\([1,l-1]\)当中即可。
常规题目
稍微常规一点的题目,没有特别 \(trick\) 。
BSOJ3876【BZOJ3207】花神的嘲讽计划Ⅰ
哈希过后发现本质就是查询一个区间内是否含有某个数,直接主席树解决。
BSOJ3401【模拟试题】建筑
本质就是询问一个下标区间\([l,r]\),且权值在值域区间 \([p,q]\) 的第 \(k\) 大的数。
可以直接主席树上二分解决。
BSOJ2895【BZOJ2223】PATULJCI
直接主席树上二分即可,因为是一半,所以必然只会有一边区间满足,要不然就是两边都不满足。
BSOJ1583【BZOJ3524】Couriers
同上。
BSOJ2251【BZOJ3744】Gty的妹子序列
询问区间逆序对,强制在线。
可以先离散化,然后分块维护整块,对于散块主席树维护。
存在在线的分块\(O(n\sqrt{n})\)做法,和同样复杂度的离线莫队做法。
CF853C Boredom
稍微容斥一下就会变成一个二维数点问题,可以直接主席树。
P3168 [CQOI2015]任务查询系统
主席树模板,其实就是对于每一个下标维护一下值域线段树,然后线段树上二分查询全局第 \(k\) 小即可。
BSOJ2781【模拟试题】区间第k大(版本2)
单点修改区间第 \(k\) 大,主席树套树状数组维护。
不想写了,离线整体二分做的。
P3332 [ZJOI2013]K大数查询
可以整体二分+线段树,也可以主席树+树状数组,相对于常规的就是多了一个区间修改而已。
经典题/Trick题/妙题
P2839&BSOJ3867 [国家集训队]middle
关于中位数的常用套路:二分中位数,然后小于的变成 \(-1\) ,剩下的变成 \(1\) ,然后数和即可,和大于0证明当前数小了,和小于0证明当前数大了。
但是在这道题当中,相当于要让中位数尽可能大,于是我们就要尽可能多的取到 \(1\) ,那么我们这道题因为中间的区间是固定的,那么变化的就只有前后两边。
那么我们考虑线段树维护,但是这样的话我们每次询问都要二分再维护一次,这样很慢。
然后我们发现其实 \(mid\) 的变化量如果只有 \(1\) 的话,只有很少位置会改变,于是考虑可持久化线段树,对于每一个 \(mid\) 值维护一个线段树。
然后直接去线段树上询问即可。
太妙了,这道题可以说是完美应用到了可持久化的性质。
P4602&BSOJ5586 [CTSC2018]混合果汁
和上一题有异曲同工之妙,这道题可以离线,那么也可以使用整体二分来做,这里讲主席树做法。
显然答案具有单调性,于是就可以二分,那么我们直接考虑对每一个值建立线段树,但是开不下,所以考虑主席树,然后最后求答案就在每一个询问上二分然后线段树上二分即可。
同样是巧妙地应用了主席树,感觉这类二分题都能这样做?
P4587&BSOJ6169 [FJOI2016]神秘数
这题比较 \(trick\),首先我们考虑怎么来求神秘数,发现我们对于每一个数可以这样来更新:
对于 \(x\) ,如果我们可以得到前 \(nowsum\ge x\),那么我们就可以得到:前\(nowsum\to nowsum+x\) 。
那么我们就可以通过不停地维护 \(nowsum\) 直到不存在这样的 \(x\) 为止。
发现这样需要建立值域线段树,然后这里又是区间询问,所以主席树即可。
P3293 [SCOI2016]美味
按位贪心,可以可持久化\(01trie\)或者主席树,这里说主席树做法。
对于最大异或和的按位贪心,其实我们除了一般的在 \(01trie\) 上二分,还有一个就是把当前位对应的值域区间直接写出来,然后再直接维护整个值域。
也就是说,假设我们要求的是这里的 \(b_i xor (a_j+x_i)\) 。
那么相当于如果 \(b\) 的这一位是 \(1\) ,\(a_j\) 就落在 \([now+(1<<k)-x,now+(1<<(k+1))-1-x]\) 这个区间当中会让这一位成为 \(1\) ,所以我们就直接在值域线段树上查询这一段是否存在数即可。
然后这样一位一位累加下去即可。
因为这里是区间查询,所以我们需要把我们的值域线段树可持久化,也就是主席树了。
这道题对于异或和的处理比较新也比较 \(trick\) 。
P3241 [HNOI2015]开店
可以点分树,这里讲可持久化线段树做法。
先这样转化一下:
然后现在重点是求点权在一段区间内的点和 \(u\) 之间的\(LCA\)的所有\(dis\)和。
于是我们按照点权从小到大来建立可持久化线段树,每一个都维护前缀,也就是说我们如果每加进来一个点,那么我们就在这个点到根的路径上所有点加上\(1\)。
然后询问就是直接询问当前点到根的路径上的区间和,然后因为是区间询问,所以直接用 \([1,r]\) 的答案减去 \([1,l-1]\) 的答案即可。
这道题是前缀数据结构维护树上路径的 \(trick\) 。
BSOJ1781【BZOJ3514】GERALD07加强版
似乎可以直接线段树分治,但是这里强制在线,于是考虑在线做法。
我们考虑 \(lCT\) 来求出每一条边加入的时候,如果当前构成了环,那么我们就删掉环上最早出现的边,并称这条删除的边是可以“替代”当前边的,就是把当前边 \(pre[i]\) 设为 \(tmp\) (删除的边的编号)。
然后我们发现问题变成了:询问一段区间内小于某个数的个数,直接主席树即可。
这道题重点在于 \(LCT\) 的那一步转化,或许这题更应该是个 \(LCT\) 题(?)。
P4559 [JSOI2018]列队
容易发现这题如果我们保持学生的相对顺序不变,答案肯定是不劣的,所以我们就让学生保持排队后的相对顺序不变即可。
那么肯定会出现一个分界点,这个点左边的学生往右跑,右边的往左跑。
BSOJ1783【BZOJ3658】Jabberwocky
发现其实最优的策略就是只有一个颜色不包含。
于是可以考虑枚举这个颜色,然后再发现这个线一定落在点上最优,于是考虑枚举每一个点。
但是我们还要保证选的区间不包含第二个这种颜色的点,才能保证不包含这个颜色。
于是我们可以这样来决定左右边界,也就是如果现在我们要选上面,那么我们当前点对应可以选的 \(x\) 坐标区间就在 \([pre_i+1,suf_i-1]\) ,且这两个数组只是对于这个点及\(y\)坐标大于等于这个点的那些点来说的,因为这样才可以保证这个颜色完全没有。
然后我们就是一个二维数点问题了,可以可持久化线段树来做。
BSOJ5381【BZOJ3956】Count
我们发现,可以利用单调栈,对于每一个点作为 \(i\) ,求出对应的 \(j\) 的可行区间。
于是这就是询问一段区间内,且权值满足在一段区间的数的个数,那么可持久化线段树即可。
这种对于每一个左端点,求出右端点所在区间,然后可持久化线段树询问的技巧也很好用。
P3722 [AH2017/HNOI2017]影魔
和上一题比较类似,就是单调栈求出一个区间对另一个区间的贡献,一个点的贡献,然后主席树维护即可。
这道题可以离线,于是也可以离线后树状数组。
CF464E The Classic Problem
首先我们发现肯定不能直接维护因为会炸,但是这里的数以二进制给出,容易想到维护二进制串来表示我们的数。
但是这样做的话,最短路会用到的比较和做加法效率都很低了,是 \(O(n)\) 单次的。
于是考虑使用数据结构来维护这个二进制串。
发现对于动态开点线段树,其实就很好来维护这个比较,直接在线段树上二分即可。
但是加法的话就相当于要复制这个线段树,然后再给某一位加一。
等等,复制线段树?
于是我们可以想到可持久化线段树,并且这里的单次修改很少。
然后我们考虑怎么来做加法。
我们先线段树上二分找到第一个零的位置,然后把这个点单点修改成\(1\),再把前面那一段区间设置成零即可。
后面这个区间修改我们可以使用一个技巧。这里有两种实现方法,第一种是建立一个全0的树,然后直接把儿子指过去就行了,第二个办法是直接设为空节点,默认就是 \(0\) 了。
这样做复杂度是显然正确的。
然后就是最短路模板了,\(Dijkstra\) 即可,时间复杂度 \(O(nlog^2n)\)。
BSOJ3865 BZOJ2588 SPOJ10628 P2633 Count on a tree
BSOJ3865 BZOJ2588 SPOJ10628 P2633 Count on a tree
套路题,就是维护树上前缀主席树,然后询问就是树上差分的思路在线段树上二分即可。
BSOJ3405【模拟试题】路径第k大
今有一n点之无根树,点上有权,试问路径之第k大点几何?
询问路径第 \(k\) 大,可以直接建立前缀主席树,然后树上差分在线段树上二分第 \(k\) 大即可。
P4216 [SCOI2015]情报传递
单点修改,询问路径上权值小于某个数的个数。
两个做法,一是先读完直接改完所有点权,然后做静态的差分+主席树即可维护,复杂度 \(O(nlogn)\) ,另外一种是,就是树剖+主席树,这个做法就比较显然,但是劣一些,是\(O(nlog^2n)\)的。
P3302 [SDOI2013]森林
发现如果只有第一个操作那么就是可持久化线段树,上面已经提到过了。
现在有了第二个操作,其实看到“合并”就启示我们想启发式合并了,于是我们直接启发式合并,然后暴力更新小的那个树的整个子树的信息即可。
SP1487 PT07J - Query on a tree III
SP1487 PT07J - Query on a tree III
查询子树第 \(k\) 大,没什么意思,直接 \(dfn\) 序就是区间第 \(k\) 大了。
P1552 [APIO2012]派遣
左偏树题,放到这里就没做了。
CF893F Subtree Minimum Query
换成离线的话就是经典的\(dsu \ on \ tree\)了,可惜换不得。
那么在线其实我们发现这里前缀构造还是\(dfn\)序构造主席树都不行了,那么怎么办呢?
我们发现其实一个点子树内且距离它深度不超过 \(k\) 其实可以转化成两个限制,一是\(dfn\)序在一段区间内,二是深度在一段区间内。
两个限制,然后询问最小值,直接可持久化线段树维护区间最小值即可,这里按照每一个深度建立或者按照\(dfn\)序建立可持久化线段树都可以。
BSOJ4205【BZOJ4771】七彩树
可持久化线段树合并,具体见题解,懒得讲。
CF226E Noble Knight's Path & BSOJ4102【2014NOI模拟5】旅行
CF226E Noble Knight's Path & BSOJ4102【2014NOI模拟5】旅行
询问路径上权值在一段区间内,且位于路径上第 \(k\) 个满足上述条件的点的编号。
先树剖,然后不断地跳,先从\(u\to lca\) ,然后\(lca\to v\) ,直到当前重链满足要求的点的个数比当前的 \(k\) 要大,然后在这条重链上二分即可,注意方向。
思路比较简单,实现比较麻烦,看着都不想写。
P4175 [CTSC2008]网络管理
树上带修路径第 \(k\) 大。
可以\(dfn\)序直接转化成序列问题,主席树套树状数组解决,复杂度\(nlog^3n\) 。
也可以直接主席树套树状数组,复杂度\(nlog^2n\),具体见题解第一篇。
然后也可以整体二分和树上带修莫队之类的,暂不赘述。
BSOJ4445【FJMTC2015#4】神牛的养成计划
这题可以用两个 \(trie\) 直接把问题转化成:询问\((u,v)\):有多少个编号满足在第一棵树\(u\)的子树内,在第二棵树\(v\)的子树内。
两维限制,很明显是一个二维数点,可持久化线段树维护即可。