好题

trick 整合

  1. 将树上问题通过树链剖分、dfs序、括号序列这些将其刻画成序列上问题,这样就可以用线段树来维护了。线段树具体要维护什么信息与计算式直接相关,很多时候维护方法都可以归结为一些做过的类似的题。

  2. ST表可以优化建图,同时也可以优化取 \(\max\)

  3. 对于这种带修改和查询的题目,一般有两种思路:

    • 修改的时候进行区间覆盖,查询的时候单点查;
    • 修改的时候单点改,查询的时候区间查。

    当然这里的单点和区间是广义上的。

  4. 很多合并关系都可以线段树维护,虽然有时没那么显然。

  5. 按照 \(2^k\) 定长分块有很多很美好的性质,它可以保证我们的复杂度的正确性。

  6. 冒泡排序有一些性质:

    • \(a_i\) 往前移动一位的次数是 \(\min(l\sim i中>i的个数,k)\)

    • 每进行一轮冒泡排序都会把最大数往后丢(准确来说是尚未归位的最大数)。

    • \(i+k\) 个数进行冒泡排序 \(k\) 次就是将前 \(i\) 小放在前面,后 \(k\) 小丢在后面,注意\(k\) 小是全部已经归位的了,但是前 \(i\) 小不一定全都归位

    • 你也可以对于每一层值域视作 01 序列来考虑,这样可能会好理解前面的,每层进行一次冒泡排序,每个 1 都会往后走,直到撞到下一个 1,此时现在的 1 就会停下来,下一个 1 开始继续往后走。

  7. 需要分析题目一些特殊性质。冒泡排序那道题很明显是要分析出冒泡排序的重要性质,赛时我只想到了第一个性质,就是往前移的性质,但是我没有想到对于解这道题最重要的往后移的性质。往前往后是对称的,这启发我们如果分析了当前方向的性质但是并无太多做题帮助,不妨换个角度思考一下,换反方向思考分析,这句话已经说了很多遍,但是真正场上并不能深入贯彻,这是需要提高的地方。

  8. 提供了一个分析问题性质的角度,我们可以讲这种排列题转化成 01 序列来考虑,似乎比较有启发性

题目

P2056 [ZJOI2007] 捉迷藏

题目大意

\(N\) 个节点的树,初始所有节点都是黑的。支持两种操作:切换某个节点的灯颜色(黑/白),或查询当前所有黑点中相距最远的两点间的树上距离。若黑点数为 \(0\) 输出 \(-1\),若为 \(1\) 输出 \(0\)

\(N \le 10^5,\ Q \le 5\times 10^5\)

解题思路

解题思路

这道题有好多种做法:括号序列+线段树;动态点分治;ddp;根号重构;线段树维护直径;树链剖分等

法一(括号序列 + 线段树维护):\(\checkmark\)

树上括号序列(欧拉序),每个点进出都各要记录一次,按照 dfs 序遍历,从父结点进去时记为 '(',出来时记为 ')',树上两个点距离就是两点间括号序列消去匹配括号后的长度。将树上距离问题转为括号序列后就可以用线段树来维护消去匹配后的括号序列长度了,具体实现可以将左括号是为 \(-1\),右括号为 \(-2\),在之间把节点编号也加进去。

线段树节点维护括号序列,我们用 \(a\) 表示消去匹配后靠左的右括号数量,用 \(b\) 表示消去匹配后靠右的左括号数量,\(dis\) 表示该区间最长的消去匹配后长度。

这题与最长上升子序列的信息维护方式有点像,计算 \(dis\) 考虑左右儿子的 \(dis\) 还有横跨左右的,所以要记录前缀后缀,具体记录什么与 \(dis\) 怎么算的有关。

如果是横跨整个区间的 \(dis\) 应该这么算:

\(dis=la+rb+|lb-ra|=\max((la+lb)+(rb-ra),\ \ (la-lb)+(ra+rb))\)

当然,有些时候不一定会横跨整个区间,所以要记录下前缀后缀 \(\max\),具体来说对于每个节点维护 \(l_1\) 表示前缀 \(\max(a+b)\)\(l_2\) 表示前缀 \(\max(b-a)\)\(r_1\) 表示后缀 \(\max(a+b)\)\(r_2\) 表示后缀 \(\max(a-b)\),维护是平凡的,初始时左右括号的 \(l_1\cdots\) 都是 \(-\infty\)\(a,b\) 都为 0/1;节点编号这些都是 \(0\)

有个需要注意的地方,\(a,b\) 维护的是整个区间消掉匹配后的右左括号数,他们所代表的区间是整个 \()))((((\),但是比如 \(l_1\) 表示的是这个节点它的后代节点中最大的 \(a+b\),他们所代表的 \(a,b\) 可能在向上合并过程中被匹配掉了,也就是说 \(a,b\) 维护的是实时状态,而 \(l_1,l_2,r_1,r_2\) 这些存的是历史最值信息,这么说可能有点抽象,感性一点理解吧,\(a,b\) 是为了尝试更新 \(l_1\) 这些服务的。

启示:将树上问题通过树链剖分、dfs序、括号序列这些将其刻画成序列上问题,这样就可以用线段树来维护了。线段树具体要维护什么信息与计算式直接相关,很多时候维护方法都可以归结为一些做过的类似的题。

法二(线段树维护直径):\(\checkmark\)

直径有一条重要的性质:有两棵树,直径分别为 \((u,v)\)\((x,y)\) 若用一条边将两颗树连成一颗,则新树的直径两端点一定是 \(u,v,x,y\) 四个点中的两个点。

根据这条性质,我们就可以直接用线段树维护区间直径的两个端点,这里线段树维护的区间可以是任意顺序,可以是dfn序,也可以就是原序,因为线段树它的目的是维护合并直径的操作而已。

还是同样的使用 dfn + ST表求 LCA。时间复杂度 \(O(n\log n)\),常数稍大。

法三(根号重构):

但我觉得这很构式啊,还不如直接维护直径。

以下复杂度都是建立在你使用 dfn + ST表 求 LCA。

我们将每 \(B\) 次操作分块,记这 \(B\) 次操作修改涉及的点集为 \(S\),不涉及的为 \(T\),分类讨论寻找最远两个黑点 \(x,y\)

  • \(x,y\in S\),这就是最简单的找直径。跑两遍 \(dfs\) 就行了,单次时间复杂度 \(O(B)\)

  • \(x,y\in T\),也是同样的找直径,复杂度 \(O(n)\)

  • \(x\in S,y\in T\),直径有条性质:树上任意一点能到达的最远的点一定是直径的一个端点。于是我们可以在找到二的基础上直接枚举 \(S\) 中元素,复杂度 \(O(B)\)

具体来说等后面再补吧,这个做法太没前途了,还不如法二。

区间选择性覆盖 \(\max\)

题目大意

给定长度为 \(n\) 的序列 \(a\),有 \(q\) 次操作,每次操作给定 \(l,r,x\),将 \(\forall i\in[l,r],a_i=max(a_i,x)\), 求出操作完成后的最终序列。
\(n\le 10^6,q\le 10^8\)

hard version:将操作变成 \(*\),定义 \(a*b=a\times b+a+b\)

解题思路

解题思路

该题来自汪总原创,可以找他取验证代码正确性。

Hint:时间复杂度 \(O(q+n\log n)\)

与萌萌哒有异曲同工之妙,那道题是 ST表优化建图,这道题同样是用 ST表优化取 \(\max\),具体实现就是 \(O(q)\) 完成操作覆盖,\(O(n\log n)\) 完成 \(\max\) 传递。

hard version:猫树。

wjr说:猫树其实就是一根链上的点分树,其实就是对序列每次折半来做的点分树,于是我们可以用到点分树的美妙性质。

其实猫树跟线段树非常像。

我们对于序列建出猫树,大概长这样,

图片

第一层的有一个重心,就是中间黑色的那个 mid;第二层有两个重心,红色的,注意黑色的不属于;第三层有四个,绿色的,我们查询的区间 \([u,v]\),查询 \([u,v]\) 在猫树上的 lca,也就是能定义它们在第几层重心,就可以通过打 \(tag[u][k]\) 表示在第 \(k\)\(u\) 向它的重心连一个前缀/后缀的链覆盖。

图片

那么如何传递信息呢,对于每层 \(O(n)\) 算一遍前缀后缀即可,总复杂度是 \(O(n\log n)\)

用猫树可以完成easy version 以及 hard version,\(*\) 操作是满足交换结合律的。

一个树上问题

题目大意

给定一颗黑白两种颜色的树,初始全白,每次有两种操作:

  • 反转点 \(u\) 的颜色
  • 查询点 \(u\) 到根 链上距离 \(u\) 最近的黑点。
  • 查询点 \(u\) 到根 链上距离根最近的黑点(笔者自己加进来的)

要求复杂度 \(O(n+q\log n)\)

解题思路

解题思路

对于这种带修改和查询的题目,一般有两种思路:

  • 修改的时候进行区间覆盖,查询的时候单点查;
  • 修改的时候单点改,查询的时候区间查。

当然这里的单点和区间是广义上的。

我们这里不好做区间改,在这里就相当于子树覆盖,这样的话反转操作我们难以维护(还需要有历史信息),于是我们考虑变成单点改。

这种链上题目能让我们很自然的想到树链剖分,我们剖完之后,单点改相当于修改单条重链,区间查相当于查询 \(u\) 到根的多条重链。我们对每条重链都维护它上面的黑点,每次跳的时候二分来查,我们可以得到一个双 \(\log\) 的解法。

更进一步,我们只需要维护每条链上深度最小的黑点即可,对于操作 2,我们每次跳到一条重链的时候,判断该链上深度最小的黑点深度是否小于当前跳到的点,如果是,直接进这条重链查,否则继续跳。对于操作 3,维护的也是同样的,不停往上跳重链,看看当前重链深度最小的黑点深度是否小于当前点,计入答案。

由于翻转操作带来的是插入和删除,同时还需要查询最小深度点,我们可以用 set 维护,复杂度是 \(O(n+q\log n)\),当然也存在对顶堆做法,具体来说就是维护两个堆,一个堆维护当前深度最小节点,另一个维护删除的点,如果两个堆堆顶相等,就往一起 pop 掉。

启示: 对于这种带修改和查询的题目,一般有两种思路:

  • 修改的时候进行区间覆盖,查询的时候单点查;
  • 修改的时候单点改,查询的时候区间查。

当然这里的单点和区间是广义上的。

神秘数据结构题

题目大意

给定一个长度为 \(n\) 的序列 \(a\),值域为 \(V\),每次有两种操作:

  • 给定 \(l,r,x\)\(\forall i\in[l,r],a_i=max(a_i-x,0)\)
  • 给定 \(l,r\),查询区间 \(\max\)

要求时间复杂度 \(O(n\log n\log V)\)

解题思路

解题思路

首先对值域按照 \(2^k\) 定长分成不同的块,按照 2 的次幂分块有些美好的性质。对每个块以下标建立线段树(以支持从 \([l,r]\) 中拿出一些数),大概分完块长这样:

图片

对于当前 \(x\) 位于块 \(T\),那么在 \(T\) 下面的块我们不需要进行考虑,我们只需要考虑当前块以及上方那些块。

对于当前块,如果存在 \(\ge x\) 的数那么他一定会下沉到下面的块,于是我们用线段树维护最大值,每次取出看一下是否 \(\ge x\) 能够下沉,下沉就是直接单点修改。

对于上方那些块,可能有些位置减去 \(x\) 之后并不会产生下沉,这些位置在块中较大,与会死我们维护最小值,每次取出看看减去 \(x\) 之后会不会产生下沉,下沉同样是单点修改。

我们暴力取出每个点进行下沉,这样复杂度是对的。分析一下:每个点最多只会下沉 \(O(\log V)\) 次,每次找到这个点是 \(O(\log n)\) 的,总的复杂度就是 \(O(n\log n\log V)\)

启发: 这是一道非常好的定长分块题目,这个经典 trick 需要掌握。按照 \(2^k\) 分块有很多很美好的性质,它可以保证我们的复杂度的正确性。

[ABC470G] ΣШX

题目大意

给定长度为 \(N\) 的非负整数序列 \(A\),求所有连续子区间 \([l,r]\)\(\mathrm{mex}\) 之和(\(\mathrm{mex}\) 为该区间内未出现的最小非负整数)。
\(N \le 3\times 10^5,\ 0 \le A_i \le N\)

解题思路

解题思路

非常经典的一道题。

法一: \(\checkmark\)

我们先固定 \(l=1\),算出每个位置 \([1,i]\)\(mex\) 计入 \(r_i\) 中,维护每个 \(a_i\) 下一个相等的数的位置,不断移动左端点 \(l\),我们发现,会发生变化的 \(r_i\) 满足:\(i<nxt_{a_l},r_i\ge a_l\),这个可以用线段树二分找到所需要的变化的区间,再用线段树维护区间和以及区间最小值即可,复杂度 \(O(n \log n)\)

法二:

P4198 楼房重建

题目大意

给定 \(N\) 个位置,每个位置有高度 \(H_i\),初始全为 \(0\)。每天修改一个位置的高度为 \(Y_i\),从原点 \((0,0)\) 看,第 \(i\) 栋楼房可见当且仅当斜率 \(H_i/i\) 严格大于所有 \(j<i\) 的斜率 \(H_j/j\)。求每天修改后可见楼房的数量。

\(1\le N,M \le 10^5,\ 1\le X_i\le N,\ 1\le Y_i\le 10^9\)

解题思路

解题思路

经典题目。

稍微简化一下:单点修改,整体查询最长上升子序列。

因为存在修改,考虑用线段树维护区间最长上升子序列和最大值,比较难的部分就是 push_up,每次就是在右儿子线段树二分出来第一个 \(>\) 左儿子最大值的位置,然后把这个位置到右端点的答案加上左儿子答案即可完成 push_up。

启示: 很多合并关系都可以线段树维护,虽然有时没那么显然。

SS260804D. 冒泡排序二合一 (sort) \(\checkmark\)

题目大意

给定一个长度为 \(n\) 的排列 \(a\),每次有两种询问:

  1. 给定 \(l,r,k,x\),询问 \(a_l\sim a_r\) 进行 \(k\) 轮冒泡排序后,值 \(x\) 的下标是多少?

  2. 给定 \(l,r,k,x\),询问 \(a_l\sim a_r\) 进行 \(k\) 轮冒泡排序后,下标 \(x\) 的值是多少?

询问互相独立,每次冒泡排序后原序列会还原。

一轮冒泡排序就是对于 \(\forall l\le i\le r-1\) 如果 \(a_i>a_{i+1}\),那么交换 \(a_i\)\(a_{i+1}\)

\(1\le n,q\le 6\times 10^5\)

解题思路

冒泡排序有一些性质: + $a_i$ 往前移动一位的次数是 $\min(l\sim i中>i的个数,k)$。
  • 每进行一轮冒泡排序都会把最大数往后丢(准确来说是尚未归位的最大数)。

  • \(i+k\) 个数进行冒泡排序 \(k\) 次就是将前 \(i\) 小放在前面,后 \(k\) 小丢在后面,注意\(k\) 小是全部已经归位的了,但是前 \(i\) 小不一定全都归位

  • 你也可以对于每一层值域视作 01 序列来考虑,这样可能会好理解前面的,每层进行一次冒泡排序,每个 1 都会往后走,直到撞到下一个 1,此时现在的 1 就会停下来,下一个 1 开始继续往后走。

回到这道题,我们一下考虑的 \(x\) 都是在 \([l,r]\) 之间,如果不在,直接输出原来的就行,我们先考虑操作 2,就是查询下标:

我们很自然的想到运用它的性质,对 \([l,x+k]\) 求出前 \(x\) 小,但是前面也说了,前 \(x\) 小不一定顺序确定,有一个这样的解决方法:
先判断 \(a_{x+k}\) 是否是 \([l,x+k]\)\(k\) 小,如果是,那么它在只能移动 \(k\) 次的条件下会移动到位置 \(x\),如果不是那么查 \([l,x+k-1]\)\(x\) 小,因为这个位置属于后 \(k\) 小,所以它一定是有序的。

查询区间第 \(k\) 小是经典的主席树问题,\(O(n\log n)\)

现在考虑操作 1,对于区间 \([l,r]\),分类讨论一下:

  • \(x\) 大的数有 \(cnt\) 个,如果 \(cnt<k\),那么 \(x\) 肯定在冒泡排序后位于 \(r-cnt\)

  • 现在 \(cnt\ge k\),如果在区间 \([l,pos_x]\) 的比 \(x\) 大的数至少有 \(k\) 个,每一轮往前进 1,那么 \(x\) 的位置 \(k\) 轮排序后肯定位于 \(pos_x-k\)

  • 考虑完前两种情况后,只剩下在区间 \([l,pos_x]\) 的比 \(x\) 大的数个数 \(c\) 不到 \(k\) 个的情况了,现在 \(x\) 会先往前挪 \(c\) 个位置,再往后挪 \(k-c\) 次,结合前面说的第 4 条性质,每次都是移动到下一个 1 排序前的位置然后停下。具体实现上就等价于是在 \(x\) 这个值域上先找到 \(l\) 往后的第 \(k\) 小,然后减去 \(k-c\) 就是 \(x\) 往后移动到的位置,不要忘记要减去 \(c\)

所以只需要建立对于每个值域维护下标位置是0/1的主席树,支持查找第 \(k\) 小和区间和即可,复杂度同样是 \(O(n\log n)\)

详细实现看 oj 代码吧。

启发

  • 首先,需要分析一些特殊性质。这道题很明显是要分析出冒泡排序的重要性质,赛时我只想到了第一个性质,就是往前移的性质,但是我没有想到对于解这道题最重要的往后移的性质。往前往后是对称的,这启发我们如果分析了当前方向的性质但是并无太多做题帮助,不妨换个角度思考一下,换反方向思考分析,这句话已经说了很多遍,但是真正场上并不能深入贯彻,这是需要提高的地方。

  • 这题同时提供了一个分析问题性质的角度,我们可以讲这种排列题转化成 01 序列来考虑,似乎比较有启发性。

  • 这道题中出现了两种主席树的建法,一种是对不同下标维护值域,另一种是对不同值维护下标区间,做题时要灵活运用不同的维护方法。

XMS2D. 神谕(oracle) \(\checkmark\)

题目大意

给定广义线段树和初始序列 \(A\)。支持两种操作:区间加 \([l,r]\) 增加 \(k\)(标准线段树区间加,下传标记并更新节点标记);询问某个恰好为线段树节点的区间 \([l,r]\) 上的标记值。输出所有询问答案。
\(n,q \le 2\times 10^5,\ 0<A_i,k\le 10^3\)

考场思路

直接模拟,可以获得高达 20 pts。

解题思路

一下说的时间顺序就是按照操作顺序。

比较难处理的是 push_down 操作,一个节点 \([l,r]\)它的权值变化其实有两个影响因素,是它父亲最近被击破的时间 \(S\),还有它自己最近被击破的时间 \(T\)。被击破就是要往下push_down,也就是遇见了一个操作区间 \([l',r']\),操作区间与节点区间有交,但是它不包含节点区间。统计就是:维护 \(f_i\) 表示当前下标 \(i\) 这个位置累加的前缀和,那么当前询问区间在某个时间点的贡献就是 \(f_l(S)-f_l(T)\),这个可以用树状数组维护,这里的 \(l\) 也可以换成任意一个 \(i\in [l,r]\),因为这时整体 \([l,r]\) 区间加,不存在 \(l<l'\le r'<r\) 有区间加,否则 \([l,r]\) 就被击破了。

于是我们可以对每个询问区间离线下来,算出它的 \(S\)\(T\),然后就变成扫描线扫时间就行了。

那么如何算出查询操作的 \(S\)\(T\),对于每个操作,其实我们只用到它的 \(l,r\),我们用一颗线段树维护每次区间加操作 \(l,r\) 的时间点,每次就是查自己和父亲被击破的时间即可,线段树维护 \(\max\),查就是查 \(max{l\in [p+1,q],r\in [p,q-1] 的时间点最后的区间}\) 简化一下就是 \(\max\{t[l+1,r+1],t[1\sim l](以 l 为右端点),t[r\sim n](以 r 为左端点)\}\)

具体实现看代码,复杂度是 \(O(n\log n)\)

启发: 题面有点吓人,但是不要被它牵着鼻子走,这题根本不是模拟题。我们应当静下心来,考虑问题的本质,比如在这道题中,问题的本质就是 push_down 的时间,然后进行扫描线。

posted @ 2026-09-01 14:43  lcx_OIer  阅读(7)  评论(0)    收藏  举报