数据结构进阶
区间数颜色
LOJ #3751. [SDOI2009] HH 的项链
给定长度为 \(n\) 的序列,\(m\) 次询问 \([l,r]\) 内有多少不同的元素。
\(n\le 5\times 10^4\),\(m\le 2\times 10^5\)。
区间数颜色是莫队算法的经典应用,可以用莫队在 \(\Theta(m\sqrt n)\) 内解决。
P1972 [SDOI2009] HH的项链(数据加强版)
题面同上题。\(n,m\le 10^6\)。
根号算法显然难以通过。我们考虑 \(\mathrm{polylog}\) 的算法。
我们不妨对右端点扫描线(将询问按右端点从小到大排序,令 \(i\) 遍历 \(1\sim r\),并回答 \(r=i\) 的询问)。一旦以 \(i\) 为右端点的询问被处理完了,我们对 \([1,i]\) 修改就不会影响到前面的询问了。
综上,我们考虑如下的算法:
- 初始化 \(pos_i=0\),表示 \(i\) 上次出现位置是 \(0\)(即没有出现过);初始化 \(b\) 序列为全 \(0\)。
- 遍历 \(i=1\sim n\)。
- 记 \(pre=pos_{a_i}\)。令 \(b_{pre}\gets 0\)。
- 令 \(b_i\gets 1\)。
- 令 \(pos_{a_i}=i\)。
- 遍历所有 \(r=i\) 的询问,答案即为 \(b\) 序列 \([l,r]\) 的区间和。
正确性是比较显然的。
利用树状数组维护前缀和,我们在 \(\Theta(m\log n)\) 内解决了问题。
线段树与区间最值操作问题
HDU 5306 Gorgeous Sequence
给定序列 \(a\),维护以下操作。
- 查询区间和。
- 查询区间最大值。
- 给定 \(l,r,x\),\(\forall i\in [l,r]\),令 \(a_i\gets \min(a_i,x)\)。
\(n,m\le 10^6\)。
线段树与历史最值问题
P4314 CPU 监控
给定序列 \(a\),维护以下操作。定义辅助序列 \(b\),初始时 \(b=a\),每次操作完后对 \(\forall i\in [1,n]\),令 \(b_i\gets \max(a_i,b_i)\)。
-
查询 \(\max_{i=l}^{r} a_i\)(区间最值)
-
查询 \(\max_{i=l}^{r} b_i\)(历史最值)
-
\(\forall i\in [l,r]\),\(a_i\gets a_i+v\)(区间修改)
-
\(\forall i\in [l,r]\),\(a_i\gets v\)(区间覆盖)
\(n,m\le 10^5\),\(|a_i|,|v|\lt 2^{31}\)。
先只考虑加操作。对于一个节点(记最大值为 \(v\),历史最大值为 \(h\)),影响到它的操作可以放进一个队列:
考虑逐个处理这些操作的过程。当处理到操作 \(i\) 的时候,这个节点的最大值一定会变成 \(v+add_1+add_2+\cdots+add_i\)(我们引入 \(add\) 序列的前缀和序列 \(sum\),则改记为 \(v+sum_i\)),但是历史最大值则不然。
为什么历史最大值不一定是 \(v+sum_i\)?因为 \(v+sum_i\) 不一定是出现过的最大值。 \(add_i\) 可以为负数,从而 \(sum_i\) 不具有单调性。 但是我们可以发现,历史最大值一定是 \(\max\left(h,v+\max_{j=1}^{i}sum_j\right)\)。于是我们可以维护 \(mx=\max_{j=1}^{k}sum_j\)。
我们当然不可能模拟这个队列,不过现在我们已经可以维护新元素的入队过程了。当加入一个新的加操作 \(add\) 的时候:
(想一想:一定要按照这个顺序更新吗?为什么?)
那么我们考虑两个队列的拼接,在线段树上体现为将父节点 \(i\) 的标记下放到子节点 \(l\)(将 \(add_i\) 接到 \(add_l\) 后面)。
子节点标记:
父节点标记:
接起来:
考虑这个新的队列的前缀和。
考虑这个新的 \(mx_l\)。
即
于是我们就能够处理两个队列的拼接了。
具体地,对于队列 \(add_l\) 和 \(add_i\) 的合并:
于是加操作被解决了。
加上覆盖操作,我们有几种实现方式:定义新运算,挖掘性质。
定义新运算
我们将题目中的两种操作统一成一种操作 \((a,b)\),表示将 \(v\) 改为 \(\max(v+a,b)\)。那么区间覆盖就是 \((-\infty, x)\),区间加就是 \((x,-\infty)\)。
我们将\(v=\max(v+a,b)\) 的操作记为 \(v=v\times (a,b)\)。
先考虑两个标记的合并。\(v\) 依次 apply \((a,b)\) 与 \((a',b')\) 后变成了 \(\max\left(\max(v+a,b)+a',b'\right)=\max\left(v+a+a',b+a',b'\right)\),与 apply 标记 \((a+a',\max(b+a',b'))\) 等效。故 \((a,b)\odot (a',b')=(a+a',\max(b+a',b'))\)。
我们还是和之前一样写出某节点的操作队列。
于是,\(k\) 次操作后,我们的 \(v\gets v\times \left(op_1\odot op_2\odot\cdots\odot op_k\right)\)。而历史最大值 \(h\) 更新为 \(\max(h,h+sum_{op}\text{ 中最大的 }a,sum_{op}\text{ 中最大的 } b)\)。
我们不妨定义 \(\max\left((a,b),(a',b')\right)=(\max(a,a'),\max(b,b'))\)。于是每次入队一个新元素的时候,我们令
其中 \(tag=(a,b)\) 意为 \(sum_{op}\) 中最大的 \(a\) 和 \(sum_{op}\) 中最大的 $ b$。
如法炮制,考虑节点 \(i\) 的操作队列 \(op_i\) 下放给儿子 \(l\)(操作队列为 \(op_l\))。
于是
而这需要我们的 \(\odot\) 操作具有结合律。我们来证明一下其具有结合律。
由定义有,\((a,b)\odot (a',b')=(a+a',\max(b+a',b'))\)
则
而
证毕。
我们证明了 \(\odot\) 运算是具有结合律的,那么就可以放心玩弄啦~
那么于是 \(tag_l=\max(tag_l,sum_l\odot tag_i)\)。
于是 \(op_i\) 接在 \(op_l\) 后面的时候,我们有
(为什么是这个更新顺序?)
挖掘性质
我们注意到,如果一个节点被赋值过了,那么之后的所有加操作都可以被看成赋值操作。
于是我们分赋值前和赋值后来讨论。赋值前已经讨论过了。
赋值后,我们只需要额外维护一个 \(mx\) 表示历史最大赋值就可以了。
两种做法时间复杂度均为 \(\Theta(m\log n)\)。
P6242 【模板】线段树 3
GSS 系列
这是笔者的 AC 代码集,可供参考。
GSS1 - Can you answer these queries I
给定长度为 \(n\) 的序列 \(a\),\(m\) 次询问 \([l,r]\) 内的最大子段和(可以为空)。
\(|a_i|\le 15,007\),\(n,m\le 10^5\)。
显然用线段树维护,我们考虑在节点 \([l,r]\) 处需要维护哪些信息。
- \(mx\)。即 \([l,r]\) 内最大子段和。
- \(lmx\)。即 \([l,r]\) 内最大子段和,但是一定要选中左端点。
- \(rmx\)。即 \([l,r]\) 内最大子段和,但是一定要选中右端点。
- \(sum\)。即 \([l,r]\) 的区间和。
考虑信息的合并。以下的下标 \(l,r\) 表示左右儿子。
时间复杂度 \(\Theta(m\log n)\)。
GSS2 - Can you answer these queries II
给定长度为 \(n\) 的序列 \(a\),\(m\) 次询问 \([l,r]\) 内的最大子段和(可以为空),但是相同的数只计算一次。
\(|a_i|\le 15,007\),\(n,m\le 10^5\)。
受 P1972 的启发,由于相同的数只出现一次,套路地考虑扫描线。
套路地遍历 \(i=1\sim n\),记录 \(pos_x\) 为当前(不超过 \(i\) 的)最大的使得 \(a_k=x\) 的 \(k\)。
定义辅助数组 \(b\),起初为全 \(0\)。当 \(i\) 自增 \(1\) 时:
- 记 \(pre=pos_{a_i}\)。对 \(\forall k\in [pre+1,i]\),令 \(b_k\gets b_k+a_i\)。
- 令 \(pos_{a_i}\gets i\)。
此时,对于右端点 \(r=i\) 的所有询问,查询 \([l,r]\) 的历史最大和就是答案。
时间复杂度 \(\Theta(m\log n)\)
GSS3 - Can you answer these queries III
给定长度为 \(n\) 的序列 \(a\),\(m\) 次操作:
- 查询 \([l,r]\) 内的最大子段和(可以为空)。
- 或单点修改。
\(|a_i|\le 15,007\),\(n,m\le 10^5\)。
GSS 1 的双倍经验,只需要加上单点修改就可以了,而单点修改是容易的。
GSS4 - Can you answer these queries IV
给定长度为 \(n\) 的序列 \(a\),\(m\) 次操作:
- 查询区间和。
- 或 \(\forall i\in [l,r]\),令 \(a_i\gets \lfloor {\sqrt a_i}\rfloor\)。
\(\sum a_i \le 10^{18}\),\(n,m\le 10^5\)。
我们有经典结论:一个数在至多 \(\Theta(\log \log V)\) 次操作 2 后变成 \(1\)。于是我们对区间维护一个全 \(1\) tag,然后对于不是全 \(1\) 的区间暴力递归即可。时间复杂度据说是 \(\Theta(n\log \log V+m\log n)\) 的(by @skip2004)。
本题有 BIT+dsu 的解法。
GSS5 - Can you answer these queries V
给定长度为 \(n\) 的序列 \(a\),\(m\) 次操作:
- 给定 \(x_1,y_1,x_2,y_2\),查询 \([l,r]\) 内的最大子段和,其中 \(l\in [x_1,y_1]\),\(r\in [x_2,y_2]\)。不保证 \([x_1,y_1]\) 与 \([x_2,y_2]\) 不重合。
- 或单点修改。
\(|a_i|\le 15,007\),\(n,m\le 10^5\)。
我们来进行分类讨论。以下的交、并集运算在整数意义下进行,例如 \([1,3]\cup [4,5]=[1,5]\)。
- \([x_1,y_1]\cap [x_2,y_2]=\varnothing\)。
这时,\([y_1+1,x_2-1]\) 中间的内容是必选的(注意这个区间可能是空集)。我们查询 \([y_1+1,x_2-1]\) 的区间和,加上 \([x_1,y_1]\) 的最大后缀和 与 \([x_2,y_2]\) 的最大前缀和即为所求。
- \([x_1,y_1]\cap [x_2,y_2]\neq \varnothing\)。
显然 \([x_1,y_1]\cap [x_2,y_2]=[x_2,y_1]\)。
此时,答案分三种情况:
- \(l\in [x_1,x_2)\),\(r\in [x_2,y_2]\)
查询 \([x_1,x_2)\) 的最大后缀和 与 \([x_2,y_2]\) 的最大前缀和即可。
- \(l\in [x_1,y_1]\),\(r\in (y_1,y_2]\)
查询 \([x_1,y_1]\) 的最大前缀和 与 \((y_1,y_2]\) 的最大后缀和即可。
- \(l,r \in [x_2,y_1]=[x_1,y_1]\cap [x_2,y_2]\)
查询 \([x_2,y_1]\) 的最大子段和即可。
综上,我们在 \(\Theta(m\log n)\) 内解决了本题。
GSS6 - Can you answer these queries VI
给定长度为 \(n\) 的初始序列 \(a\),\(m\) 次操作:
- 查询 \([l,r]\) 内的最大子段和。
- 或在任意位置插入一个元素。
- 或单点修改。
- 或在任意位置删除一个元素。
\(|a_i|\le 15,007\),\(n,m\le 10^5\)。
有插入删除操作,想到用平衡树维护。这里以 fhq-Treap(其实 Splay 也一样)为例。
对于每个节点,我们除了维护最大子段和的那堆 tag,还需要维护一个编号加的 tag。
插入时,我们 Split 后,对 Split 出来的右半区间打上区间子段和 +1 的 tag。删除时就打上 -1 的 tag。然后就做完了。
时间复杂度 \(\Theta(m\log n)\),常数较大。
GSS7 - Can you answer these queries VII
给定一棵 \(n\) 个节点的树,点有点权。\(m\) 次操作:
- 查询 \((u,v)\) 路径上的最大子段和,可以为空。
- 或对 \((u,v)\) 路径上的点进行点权覆盖。
\(n,m\le 10^5\),\(|a_i|\le 10^4\)。
直接考虑树链剖分。对于拆出来的两个区间再合并一次即可,注意合并的方向。
对于点权覆盖操作,是很好实现的。时间复杂度 \(\Theta(m\log ^2 n)\)。
GSS8 - Can you answer these queries VIII
给定长度为 \(n\) 的初始序列 \(a\),\(m\) 次操作:
- 给定 \(l,r,k\),查询 \(\displaystyle \sum_{i=l}^r a_i\cdot (i-l+1)^k\) 的值,对 \(2^{32}\) 取模。
- 或在任意位置插入一个元素。
- 或单点修改。
- 或在任意位置删除一个元素。
\(0\le a_i\lt 2^{32}\),\(n,m\le 10^5\),\(0\le k\le 10\)。
首先先上个平衡树。由于 \(k\) 很小,所以可以对每个 \(k\) 单独统计答案。
考虑如何合并答案。
左边区间的贡献是容易的。但是对于右边区间,
假设左边区间的长度为 \(x\),我们得到
注意到第二个和式就是 \(k=j\) 的时候的答案,边界是 \(k=0\) 时退化成区间和。这样我们就在 \(\Theta(k)\) 的时间内完成了区间合并,总时间复杂度 \(\Theta(mk\log n)\)。
莫队
P5071 [Ynoi2015] 此时此刻的光辉
Yuno OI 四血祭。
Chtholly 给你了一个长度为 \(n\) 的正整数序列 \(a\),\(m\) 次查询
对 \(19,260,817\) 取模。
其中 \(\sigma_0\) 表示约数个数函数。\(n,m\leq 10^5\),\(1 \leq a_i \leq10^9\)。
我们有经典的结论
其中 \(c_i\) 为 \(n\) 的唯一分解中第 \(i\) 个素数的指数。
类似上题莫队的做法,我们考虑根号分治。具体地,设置一个阈值 \(B\),对于不大于 \(B\) 的素数,我们维护其 \(\sigma_0\) 的前缀积;对于大于 \(B\) 的素数,我们用 hash 表记录下其指数,直接在莫队的时候处理。为了让复杂度正确,需要预处理乘法逆元。
我们可以将 \(B\) 设置成 \(10^3\) 左右(这样 \(\le 10^3\) 的素数有 \(168\) 个)。时间复杂度为 \(\displaystyle\Theta\left(n\sqrt n+m\frac{\sqrt B}{\log \sqrt B}\right)\)。
需要中等程度的卡常。如果 unordered_map
被卡可以用 __gnu_pbds::gp_hash_table/cc_hash_table
。
平衡树
平衡树主要用 FHQ Treap / Splay 实现。
P5350 序列
维护一个长度为 \(n\) 的序列 \(a\),支持以下操作。
- 求区间和。
- 区间赋值。
- 区间加。
- 用一个区间填充另一个区间。形式化说,给定 \([l_1,r_1]\) 和 \([l_2,r_2]\),对 \(\forall i\in [0,r_1-l_1]\),令 \(a_{l_2+i}=a_{l_1+i}\)。
- 交换两个区间。给定 \([l_1,r_1]\) 和 \([l_2,r_2]\),对 \(\forall i\in [0,r_1-l_1]\),交换 \(a_{l_1+i}\) 与 \(a_{l_2+i}\)。
- 区间翻转。
最后需要输出整个序列。
保证数据随机生成,保证数据合法(涉及两个区间的操作,这两个区间长度相等且无交),所有答案对 \((10^9+7)\) 取模。
\(n,m\le 3\times 10^5\)。
直接利用 fhq Treap 维护即可。时间复杂度 \(\Theta(m\log n)\)。
本题也有使用珂朵莉树的做法。
根号分治
P7811 [JRKSJ R2] 你的名字。
左偏树
P3261 [JLOI2015] 城池攻占
给定一棵 \(n\) 个节点的有根树,根节点为 \(1\)。第 \(i\) 个节点的防御值为 \(h_i\);有 \(m\) 个攻击者,第 \(i\) 个人初始攻击力为 \(s_i\),起始攻击节点为 \(c_i\)。
一个节点被攻占,当且仅当攻击者的战斗力不小于节点的防御值;否则我们说攻击者在这个节点牺牲。攻占第 \(i\) 个节点后,攻占者的战斗力会乘以 \(v_i\) 或加上 \(v_i\),而且会尝试攻占当前节点的父节点,直到牺牲或者攻占 \(1\) 号节点为止。
每个攻击者的攻击是独立的。对于每个节点,输出有多少攻击者在此牺牲;对于每个攻击者,输出他攻占了多少个节点。
\(n\le 3\times 10^5\)。保证任何时候,战斗力都在 C++ long long
范围内。对于乘法的节点,保证是乘以一个正数。
我们 dfs 整棵树。我们将每个节点设置一个小根堆,初始时将 \(c_i=u\) 的节点压到点 \(u\) 的堆里面。
处理每个节点的时候,我们先将不满足条件的攻击者弹掉,顺便统计信息;在根节点上打上 tag,然后将这个节点对应的堆与父节点合并。
为了方便起见可以设置一个不可能被击败的 \(0\) 号节点。认为 \(n,m\) 同阶,我们在 \(\Theta(n\log n)\) 的时间复杂度内解决了问题。
猫树
待补。
Reference
一种神奇的数据结构——猫树 by Jμdge