log数据结构乱做
高级的线段树题考的都不是线段树,更多是其他算法底层的本质,然后用线段树来优化。
LCT 自己画画图手摸一下看看这样打通或旋转对不对。
SPOJ QTREE系列
这个系列题目内容主要为树上路径/子树问题,常见处理方法为 LCT 和树剖、树上倍增。
Qtree1
LCT。很 naive 的想法是对边重新开点,维护路径最大值,查询直接将路径拉出来即可。代码
Qtree2
multiply。路径和是平凡的。求祖先先求出 \(lca\),分类讨论是 \(x\) 还是 \(y\) 的祖先,然后就可以倍增求了。代码
Qtree3
LCT。维护的东西是链上最上段 \(1\) 的位置,支持单点翻转,这个 splay 上来直接修;查询把单点到 \(1\) 号点的链拉出来查即可。代码
Qtree4/[ZJOI2007] 捉迷藏
LCT+multiset。先建出静态的 LCT,没有修改的话边权转点权即可。实链和虚子树上的东西维护方法并不相同,存在的情况赋值,不存在的情况设为 \(-\infty\) 即可:实链维护一棵 splay 中链头和链尾到白点的最远距离,这个东西直接由 pushup 向上合并;虚子树维护每个虚儿子距离白点的距离,以及每个虚儿子的 splay 中的两白点距离,这样每次获取虚儿子的信息我们只要择取最优的,可以使用 multiset 维护。这样一个点的答案可能是 splay 中的左儿子/右儿子/虚儿子直接转移而来,也可能来自两个不同儿子的单条路径拼合而成,大力分类讨论转移即可。黑白点的区别仅在于转移的时候要不要考虑他自己。修改的时候把点 splay 上来,改完之后直接更新答案取根的值,这样套出来复杂度是 \(\mathcal O(n \log^2 n)\) 的。代码
SPOJ GSS系列
这个系列题目内容以维护区间最大子段和为主线。维护这个一般需要维护区间和,区间最大前缀,区间最大后缀,区间最大子段和四个信息。使用结构体封装和重载运算符可以使代码非常好看。
struct node
{
int sum,mix,pre,suf;
node(int Sum=0,int Mix=0,int Pre=0,int Suf=0):sum(Sum),mix(Mix),pre(Pre),suf(Suf) {}
inline friend node operator + (node a,node b)
{
node res;
res.sum=a.sum+b.sum;
res.pre=max(a.pre,a.sum+b.pre);
res.suf=max(b.suf,b.sum+a.suf);
res.mix=max(max(a.mix,b.mix),a.suf+b.pre);
return res;
}
};
GSS1
SGT+GSS。把树建出来直接查询即可。查询写的可能和正常的查询不大一样,分成完全在左区间,完全在右区间,跨区间三种情况查询——因为重载了运算符要注意运算顺序。代码
GSS2
SGT。和 GSS1 关系不大。如果我们需要在一个区间内去重,就必然要知道这个数上一次出现在什么位置,预处理好这个信息记为 \(pre_i\),那当我们有序遍历序列,加入一个数后能造成影响的区间就只有 \(pre_i \sim i\)。所以我们维护区间和以及区间历史最大和,这个地方不需要势能线段树,魔改一下操作就行。整个流程变为:离线询问,从左到右扫一遍序列,到达一个节点上后,将 \(pre_i \sim i\) 加上 \(val_i\),处理右端点为 \(i\) 的所有询问,即询问 \(l_i \sim i\) 的历史最大值。代码
GSS3
SGT+GSS。加上了单点修改,和 GSS1 一样写,修改暴力向上合并即可。代码
GSS4
SGT。注意到开方操作次数不会太多,维护区间最大值,超过 \(1\) 直接暴力修改区间。代码
GSS5
SGT+GSS。分类讨论。当 \(r_1 < l_2\) 的时候,\([r_1,l_2]\) 是一定要被选的,求 \([l1,r1)\) 的最大后缀和 \((l2,r2]\) 的最大前缀;若 \(r_1 \geq l_2\),则答案有三种情况,分别是 \([l_2,r_1]\) 的最大子段和,\([l_1,l_2)\) 的最大后缀加 \([l_2,r_2]\) 的最大前缀,\([l_1,r_1]\) 的最大后缀加 \((r_1,r_2]\) 的最大前缀。为了下面的操作写的好看一点,查询函数里面加个判断 if(l>r)
。注意区间开闭防止算重。代码
GSS6
BST+GSS。插入删除是平凡的平衡树操作,在平衡树上每个节点再额外维护一个最大子段和信息就好了,其他的和普通序列操作一样。代码
GSS7
SGT+GSS+Heavy Path Decomposition。树剖套上GSS3。注意链上的最大子段和合并,不能像普通信息一样直接 swap
,必须要两个点各维护一段,从下往上跳;最后把一段翻转过来再合并得到答案。代码
GSS8
BST+GSS+Cnum。前三个操作是平凡的平衡树操作,考虑询问的答案统计。颓式子:
这样就可以合并区间答案了。注意到 \(k\) 很小,直接每个节点存下所有 \(k\) 的答案。预处理组合数,然后魔改一下 pushup 就可以直接统计了。代码
杂题
[CF240F] TorCoder
SGT。能够构成回文串的充要条件是最多有一个字符出现了奇数次,剩余字符都是出现了偶数次。保证重排后字典序最小,贪心把小的放在前面即可。需要查询一个区间上每种字符出现次数,区间推平,考虑使用线段树维护,时间复杂度 \(\mathcal O(|S| m \log n)\),其中 \(|S|\) 为字符集大小,此处为 \(26\)。代码
[CF452F] Permutation
SGT+Hash。首先只需要保证存在长度为 \(3\) 的等差数列。注意到给出的是排列,这具有很好的性质,我们可以枚举中项,看另外两个数在序列上是否在这个位置的同一侧。从左到右扫一遍序列,动态维护一个 \(01\) 串,每次将 \(a_i\) 在串上标为 \(1\)。如果此时 \(01\) 串为回文串,则证明无解,否则有解。如何快速查询和修改呢?两棵线段树维护正反串的哈希值即可快速判断是否回文。代码
[Ynoi2013] 无力回天 NOI2017
SGT+LineBasement。这个查询自然联想到线性基,又是区间上的操作,使用线段树套线性基。记原序列为 \(A\) 并钦定 \(A_0 = 0\),异或差分数组为 \(C_i = A_{i-1} \oplus A_{i}\)。考虑维护异或差分数组,这样区间异或操作就转化成了单点异或。注意到 \(C\) 还原 \(A\) 式子为 \(A_i = C_1 \oplus C_2 \oplus \cdots \oplus C_i\),所以 \(C_{i+1} \oplus A_i = A_{i+1}\),并以此类推,得到 \(A_l \sim A_r\) 的线性基等价于 \(C_{l+1} \sim C_r\) 的线性基再插入 \(A_l\),直接线段树维护 \(C\) 数组,查询然后线性基上贪心求答案,特判掉 \(l = r\) 的情况即可。代码
[CF522D] Closest Equals
SGT。先离散化,预处理每一个位置上的数上一次出现在什么位置。离线询问并按照右端点排序,下标从左向右扫,如果当前位置上有前驱,就将前驱位置上的值改成当前位置到前驱的距离,询问就是区间最小值,可以直接线段树维护。代码
[POI2015] LOG
BST。在序列 \(A\) 上每次选出 \(c\) 个正数,并将它们都减去 \(1\),询问能否进行 \(s\) 次操作。将大于等于 \(s\) 的数记为 \(g_i\),小于 \(s\) 的数记为 \(l_i\),\(sum = \sum_{i=1}^n A_i \times [A_i < s]\),\(num = \sum_{i=1}^n [A_i \geq s]\)。考虑对于 \(g_i\) 我们可以把他们放在每次操作中,而 \(l_i\) 只能在一定的操作中使用,每次我们已经有 \(num\) 个位置上填了 \(g_i\),所以有 \(c - num\) 个位置要填 \(l_i\),使用 \(l_i\) 次数至少要达到 \((c - num) \times s\) 次,综上所述有解的充要条件是 \(sum \geq (c - num) \times s\)。需要维护 \(sum\) 和 \(num\),使用无脑的平衡树实现,离线+BIT可以快好几倍。代码
[LG-P3765] 总统选举
SGT+BST+BMV。区间绝对众数,摩尔投票具有可加性,用线段树维护。找到区间众数后需要验证众数是否合法,即严格大于区间长度的一半,考虑使用平衡树维护,每个人开一棵平衡树记录支持他的人的编号,然后就是求一段值域上有多少个数。修改操作不超过 \(10^6\) 次,一个一个改就好了。代码
[SCOI2015] 情报传递
BIT+Heavy Path Decomposition。操作一为设定树上某一个点从某一个时刻开始持续 \(+1\),操作二为查询某一时刻两点路径上有多少个点的权值大于 \(k\)。设当前时刻为 \(T\),则对于一个询问,符合条件的点必然从 \(T - k - 1\) 时刻之前开始操作一,所以将询问离线直接在 \(T - k - 1\) 时刻查询就好了。本质上是三维数点,由于第一步的转化使得有两维的值相同,就变成了二维数点。先树剖,然后使用差分,操作为子树加,单点查,可以树状数组到一只 \(\log\)。代码
[GXOI/GZOI2019] 旧词
SGT+Heavy Path Decomposition。
首先考虑弱化版:[LNOI2014] LCA。对于点对 \((x,y)\),他们的 \(\text{LCA}\) 的深度等价于从根到两个节点的公共路径长度。所以如果要单个求的话,就可以将 \(x \to root\) 上的节点都加 \(1\),然后求 \(y \to root\) 的点权和,我们称其为 \(Ans(x \to y)\)。题目中的式子再差分一下就可以变成求出 \(\sum^r_{i=1} Ans(i \to z) - \sum^{l-1}_{i=1} Ans(i \to z)\)。怎么一次性求多个这种东西呢?离线询问,套路的在位置上存问题的序号,枚举 \(1 \sim n\) 到根路径上点值都 \(+1\),然后对应的询问。代码
再回来考虑本题,相当于固定了 \(l = 1\),每次给定 \(r\) 和 \(z\),但是求的是 \(dep^k\)。观察到从 \(dep^1\) 到 \(dep^k\) 单个点 \(x\) 的变化量从 \(dep_x - (dep_x - 1) = 1\) 变成了 \(dep_x^k - (dep_x - 1)^k\),预处理一下幂,线段树上修改多乘上一个这个东西就好了。代码
[CF620E] New Year Tree
SGT+bitmask+dfs。只有子树操作,先求 dfs 序,然后在上面建线段树区间操作。颜色数量很少,考虑直接状压记录每种颜色是否存在,然后就是平凡的区间覆盖区间查询,合并区间改成或运算就好了。判断二进制下有几个一可以直接用 STL 的 builtin_popcount。代码
[WC2005] 双面棋盘
SGT+DSU。以行为单位在上面建立线段树,注意到合并两个线段树区间,如果记左儿子为靠上的一段,右儿子为靠下的一段,那么相关的颜色信息只由左儿子的最下面一行和右儿子的最上面一行,所以在每个节点上记录的是这个节点维护的几行中最上面和最下面两行的“归属”信息,以及当前总共有多少个白/黑格子。合并区间就是 \(\mathcal O(n)\) 的暴力合并,“归属”信息可以用并查集很好的维护:当相邻两个格子颜色相同时放入到一个并查集中,如果之前两个格子不在一个并查集中,就更改答案信息。单点修改直接暴力重构即可。代码
[CF1681F] Unique Occurrences
SGT divide。考虑“恰好经过一条边一次”,根据乘法原理和加法原理一种颜色的贡献实际为在树中删掉该种颜色所有的边后,每条这种边所连接的两个连通块的大小乘积之和。删除操作不好搞,对于删除颜色 \(k\) 可以转化为线段树分治,即颜色 \(k\) 在 \([1,k-1]\) 和 \([k+1,n]\) 两段中存在,可撤销并查集简单维护连通块大小即可。代码
[CF765F] Souvenirs/[CF1793F] Rebrending/[JSOI2009] 面试的考验
SGT+Dominant pair。一维最近点对。没有修改先离线询问挂在右端点上。对于位置点对 \((i,j)\),只考虑 \(i < j,a_i < a_j\),对于 \(i < j,a_i > a_j\) 的情况只需要翻转值域再做一遍即可。使用支配对的套路,注意两两为一对会形成一个贡献,总共有 \(n^2\) 对,但实际上这样考虑:记 \(j_1\) 是 \(i\) 后面第一个有贡献的点,则 \(a_{j_1} \in [a_i+1,inf]\) 且 \(j_1 \in [i+1,n]\);\(j_2\) 是 \(i\) 后面第二个有贡献的点,则 \(a_{j_2} \in [a_i+1,\dfrac{a_i + a_{j_1}}{2}]\) 且 \(j_2 \in [j_1+1,n]\),每次贡献值域上都会减半,所以实际上有贡献的点只有 \(n \log n\) 对。寻找合法的 \(j\) 可以使用权值线段树,值域过大直接动态开点。查询答案在树状数组上查询后缀最小值即可。两两相等的特殊性质特判一下。代码
[CF266E] More Queries to Array...
SGT。有点像 GSS8,还是考虑二项式定理,拆式子:
注意到 \(k\) 很小,对于每一个 \(k\) 维护 \(\sum_{i = l}^r a_i (i + 1)^j\),区间推平的内容可以前缀和预处理存好,直接赋值即可。线段树可以轻松维护。代码
[CF487E] Tourists
BFDS+SGT+Heavy Path Decomposition。路径必然是从一个点出发,走到一个点双里头会把点双里面每个点都走一次。这个性质很好,直接在圆方树上做,方点存的是点双里的最小值,就转化成了树上路径问题,树剖线段树可以轻松维护。但是问题在于修改如何做,如果暴力改的话会被菊花卡到 \(\mathcal O(n^2)\),一个技巧是方点只存树上儿子节点里面的值,这样单点修改只需要再改圆点的父亲方点;查询的话如果 LCA 是方点的话在额外查一下方点的父亲即可。代码
[NOI2012] 魔幻棋盘
SGT+SGT。一维上的注意到 \(\gcd(a,b,c) = \gcd(a,b-a,c-b)\),线段树维护差分转化为单点加区间查。二维平面上使用二阶差分,用二维线段树(线段树套线段树或四分树)即可;额外维护棋盘守护者所在的行和列,以及单点的修改,用两棵线段树是平凡的。代码
[HNOI2010] 城市建设
SGT Divide+LCT。动态维护最小生成树是典的 LCT 套路,注意到每段边边在一段连续时间出现,考虑使用线段树分治维护边出现的时刻,在线段树上插入时用个前驱数组记录一下出现时间即可,属于是两个板子套一块了。代码