DS 基础
DS 基础
摘录 \(2025.12 \sim 2026.1\) 训到的 DS
cdq 分治
总结:
-
常见问题形式:
- 点对计数
- 多维偏序
- 维护 DP 转移
-
要求:
-
静态问题 (修改全部在询问前) 可做,且复杂度与操作次数相关
-
可以将修改分批次贡献
-
-
Trick:
-
外层排序降维 + 保证段间相关值的大小关系 (如限定 \(\min\) / \(\max\) 取在哪段)
-
两段分别排序 + 双指针降维 / 利用单调性维护信息 (如斜优 DP)
-
多层 cdq 多次降维
-
修改有覆盖关系时,考虑差分消除影响
-
前半段 DP 值看成修改,求后半段 DP 值看成查询,维护段间 DP 转移
-
*P14638 [NOIP2025] 序列询问 / query (cdq 分治 + 分析复杂度 - O(nR/L) + 倍增分块预处理)
给定长度为 \(n\) 的整数序列 \(a_i\)
有 \(q\) 次询问,第 \(k\) 次会给出 \(L_k, R_k\),对所有 \(1 \le i \le n\),求出 \(\max\limits_{1 \le l \le i \le r \le n} \{ \sum\limits_{j=l}^{r} a_j\ |\ L_k \le r-l+1 \le R_k\}\)
\(1 \le n \le 5 \cdot 10^4\),\(1 \le q \le 1024\),\(|a_i| \le 10^5\),\(1 \le L_k \le R_k \le n\)
考虑 cdq 分治
设分治区间为 \([l, r]\),我们需要统计所有跨过 \(\text{mid}\) 的区间的贡献
考虑如何确定对位置 \(i \in [l, \text{mid}]\) 的贡献,\(i \in [\text{mid}+1, r]\) 是同理的
枚举区间左端点 \(l'\ (l \le l' \le \text{mid})\),要求 \(r'\) 跨过 \(\text{mid}\),显然 \(r'\) 的取值范围为一段区间,ST 表维护前缀和即可
时间复杂度 \(O(qn \log n)\)
考虑另一种分析复杂度的方式:
-
显然,\(r-l+1 < L_k\) 的区间可以直接扔掉
设分治到第 \(p\) 层时区间长度第 \(1\) 次 \(< L_k\),即有 \(\frac{n}{2^{p-1}} \ge L_k \land \frac{n}{2^{p}} < L_k\)
因此,分治区间个数 \(\le 2^0 + 2^1 + \cdots + 2^{p-1} = 2 \cdot 2^{p-1} - 1 \le 2 \cdot \frac{n}{L_k}\)
-
处理 \(1\) 个区间的复杂度为 \(O(R_k)\),总复杂度 \(O(n \frac{R_k}{L_k})\)
换句话说,\(\frac{R_k}{L_k} = O(1)\) 时,分治复杂度是线性的
这启示我们将询问区间分块,每块形如 \([2^x, 2^{x+1}]\)
将询问 \([L_k, R_k]\) 拆成 \([L_k, 2^x], [2^x, 2^{x+1}], \cdots, [2^{y-1}, 2^y], [2^y, R_k]\):
- 对每个点 \(i\) 预处理 \(f_{i, x}\) 表示询问区间为 \([2^x, 2^{x+1}]\) 时,点 \(i\) 的答案
- 对每个点 \(i\) 预处理 \(\max\limits_{l \le k \le r} f_{i, k}\),因此可以快速求出询问区间为 \([2^x, 2^y]\) 时的贡献
- 对 \([L_k, 2^x]\) 与 \([2^y, R_k]\) 暴力分治即可
总复杂度 \(O(n \log n \log \log n + qn)\)
P4169 [Violet] 天使玩偶 / SJY摆棋子 (cdq 分治)
定义两点间距离 \(\text{dist}(A, B) = |A_x-B_x| + |A_y-B_y|\)
初始给定 \(n\) 个点,有 \(m\) 次操作:
- 给定 \(x_i, y_i\),加入\(1\) 个点 \((x_i, y_i)\)
- 给定 \(x_i, y_i\),询问离点 \((x_i, y_i)\) 最近的点有多远
\(1 \le n, m \le 3 \cdot 10^5\),\(0 \le x_i, y_i \le 10^6\);4s, 512MB
考虑 cdq 分治,对分治区间 \([l, r]\),只需考虑 \([l, \text{mid}]\) 的修改对 \([\text{mid}+1, r]\) 的询问的贡献
考虑拆绝对值,两边分别按 \(a_i\) 排序,以 \(a_{l'} \le a_{r'}\) 为例:
- 枚举 \(r'\),双指针遍历 \(l'\)
- 对每个 \(l'\),拆开 \(|b_{r'}-b_{l'}|\),开两个线段树维护 \(b_{l'}-a_{l'}\) 与 \(-b_{l'}-a_{l'}\)
- 在线段树中查 \([0, b_{r'}]\) 与 \([b_{r'}, V]\) 即可
可以离散化,时间复杂度 \(O(n \log^2 n)\)
P3769 [CH弱省胡策R2] TATT (四维偏序 - cdq 套 cdq 优化 DP)
给定四维空间中 \(n\) 个点,第 \(i\) 个点坐标为 \((a_i, b_i, c_i, d_i)\)
求出一条最长路径,使得任意一维坐标均单调不降
输出路径长度 (点数);任意点只能经过一次
\(1 \le n \le 5 \cdot 10^4\),\(V \le 10^9\);2s, 512MB
合并相同点,设与点 \(i\) 相同点的个数为 \(\text{siz}_i\)
令 \(f_i\) 表示以 \(i\) 结尾的最长路径长度,转移为 \(f_i \leftarrow f_j + \text{siz}_i\ [a_j \le a_i \land b_j \le b_i \land c_j \le c_i \land d_j \le d_i]\)
转移条件为四维偏序,考虑 cdq 分治优化 DP
设分治区间为 \([l, r]\),将 \([l, \text{mid}]\) 的 DP 值看作修改,求 \([\text{mid}+1, r]\) 的 DP 值看作询问,维护 DP 更新过程
外层排序去掉 \(a_i\) 维后变为三维偏序,考虑 cdq 套 cdq:
- 对外层 cdq 分治区间 \([l, r]\),将 \([l, \text{mid}]\) 打 \(0\) tag,\([\text{mid}+1, r]\) 打 \(1\) tag
- 对 \([l, r]\) 进行内层 cdq,限制只能由 \(0\) tag 点贡献到 \(1\) tag 点 (保证 \(a_i\) 维顺序)
- 内层 cdq 前,将 \([l, r]\) 按 \(b_i\) 排序,消去 \(b_i\) 维
- 对内层 cdq 区间 \([l', r']\),类比三维偏序,左右分别排序 + 双指针消去 \(c_i\) 维,树状数组维护 \(d_i\) 维 + 转移即可
注意:
-
外层 cdq 分治顺序为 \([l, \text{mid}]\) + 跨 \(\text{mid}\) 转移贡献 + \([\text{mid}, r]\)
这样可以保证在用 \([l, \text{mid}]\) 更新前,其本身的 DP 值已被更新,总是最优的
时间复杂度 \(O(n \log^3 n)\)
偏序问题其他做法:
考虑枚举维度 \(i\),对每个点开 bitset 记录当前维偏序它的所有点
枚举每个点,对所有维的 bitset 取交,即得到能偏序该点的所有点
Bonus:
-
若贡献为 chkmax,注意到 \(1\) 个点只会被贡献 \(1\) 次,按点权从大到小枚举点,暴力更新即可
P.S. 可以使用
for (int i=bs._Find_first(); i<bs.size(); i=bs._Find_next(i))快速遍历所有 \(1\) 的位置 -
若形如维护 \(f_{u} \xleftarrow\min \min f_{v} + c_u\) (在线,转移条件为偏序),考虑点权最短路, \(1\) 个点只会被贡献 \(1\) 次
按 dijkstra 的方式,按已知点 \(f_u\) 从小到大枚举并维护最短路即可
-
本题的分块 bitset + 根号重构做法:
-
对四个维度分别排序,得到四个序列,对每个序列隔 \(\sqrt n\) 个位置设 \(1\) 个关键点,共 \(4\sqrt n\) 个关键点
-
在每个关键点处维护 \(1\) 个 bitset,表示已更新完 DP 值的点中,当前维值 \(\le\) 当前关键点值的点集
换句话说,只考虑当前维偏序,维护有哪些已更新完的点能转移至当前关键点
-
拉出转移拓扑序 (按 \((a_i, b_i, c_i, d_i)\) 的字典序排序),同样分块,从前往后更新 DP 值:
-
设当前在块 \(k\),要更新块内点 \(i\),在四个维度对应的序列上找到 \(i\) 前的第 \(1\) 个关键点
对这 \(4\) 个关键点的 bitset 取交,即得前 \(k-1\) 个块中能转移至 \(i\) 的点集
我们希望快速找到点集中的最大 DP 值,考虑修改 bitset 维护的东西:
-
首先,建立 DP 值排名 \(\rightarrow\) DP 值的双射
-
bitset 中第 \(i\) 位,维护 DP 值排名为 \(i\) 的点能否在对应维转移至关键点
这样的好处在于,bitset 中第 \(1\) 个为 \(1\) 的位置即为能转移至关键点的最大 DP 值排名
通过 DP 值排名 \(\rightarrow\) DP 值的双射,即可求出转移到关键点的最大 DP 值
综上,取交后
_Find_first()再映射下即可 -
-
对四维中第 \(k\) 个块的散点,暴力枚举,判断所有维的偏序关系 + 转移即可
-
-
当跨过块 \(k\) 到块 \(k+1\) 时,对所有 bitset 进行重构:
- 重新将拓扑序上已更新完的所有点拉出来按 DP 值排序,建立排名 \(\rightarrow\) DP 值的双射
- 暴力枚举所有四个维度上的关键点,对每个维度:
- 首先,令当前关键点的 bitset 对上个关键点的 bitset 取并
- 其次,加入当前维度当前块中已更新完 DP 值的点,将其在 bitset 中置 \(1\)
-
设维数为 \(k\),转移复杂度 \(O(\frac{n^2k}{w})\)
重构次数为 \(O(\sqrt n)\),单次重构复杂度 \(O(n \log n+ \frac{kn \sqrt n}{w})\),总重构复杂度 \(O(n \sqrt n \log n + \frac{n^2k}{w})\)
综上,总时间复杂度 \(O(n \sqrt n \log n + \frac{n^2k}{w})\),空间复杂度 \(O(\frac{kn \sqrt n}{w})\)
-
CF1045G - AI robots (cdq 分治 - 排序去掉 min)
给定参数 \(k\) 与 \(n\) 个点,第 \(i\) 个点有三个参数 \(x_i, r_i, q_i\),计数满足如下条件的点对 \((i, j)\) \((i < j)\) 个数:
- \(x_j \in [x_i-r_i, x_i+r_i]\)
- \(x_i \in [x_j-r_j, x_j+r_j]\)
- \(|q_i - q_j| \le k\)
\(1 \le n \le 10^5\),\(0 \le k \le 20\),\(0 \le x_i, r_i, q_i \le 10^9\);2s, 256MB
点对计数,考虑 cdq 分治
转化下限制,要求为 \(|x_i-x_j| \le \min(r_i, r_j)\) 且 \(|q_i-q_j| \le k\)
cdq 分治不好处理 \(\min\),考虑先按 \(r_i\) 排序,统计 \(l' \in [l, \text{mid}], r' \in [\text{mid}+1, r]\) 时必有 \(\min(r_{l'}, r_{r'}) = r_{l'}\)
枚举 \(l'\),拆开绝对值,转化为 \(x_i-r_i \le x_j \le x_i+r_i\) 且 \(q_i-k \le q_j \le q_i+k\)
差分询问 + 排序 + 双指针消去 \(1\) 维,离散化 + 树状数组维护另外 \(1\) 维即可
时间复杂度 \(O(n \log^2 n)\)
CF848C - Goodbye Souvenir (刻画答案 + 消除重复修改影响 - 差分 + cdq 分治)
给定长为 \(n\) 的序列 \(a_i\),定义 \(f(v)\) 为值 \(v\) 最后一次出现位置与首次出现位置的差
定义 \(F(l, r)\) 为区间 \([l, r]\) 内所有出现的 \(v\) 的 \(f(v)\) 之和
有 \(m\) 次操作:
- 给定 \(p, x\),将第 \(p\) 颗珠子改成 \(x\)
- 给定 \(l, r\),查询 \(F(l, r)\)
\(1 \le n, m \le 10^5\),\(1 \le a_i \le n\);6s, 256MB
令 \(\text{pre}_i\) 表示 \(i\) 位置前第 \(1\) 个与 \(a_i\) 相同的元素的位置
容易发现,对询问 \([l, r]\),答案为 \(\sum\limits_{i=l}^{r} (i-\text{pre}_i)\ [\text{pre}_i \ge l]\)
将 \(i\) 位置看成点 \((\text{pre}_i, i)\),点权为 \(i-\text{pre}_i\),限制为 \(\text{pre}_i \ge l \land i \le r\),转化为动态二维数点
对修改 \(a_p = x\),上 set 维护 \(\text{pre}_i\) 的变化,拆成若干个删点 + 加点操作:
- 删点的目的是消除前面对应位置修改的影响,这样无需考虑重复修改的问题
上 cdq 转静态即可,时间复杂度 \(O(n \log^2 n)\)
*P4027 [NOI2007] 货币兑换 (cdq 分治保证 x 单调 - 斜率优化 DP)
有 A 券和 B 券两种金券,持有金券的数目可以是实数
第 \(i\) 天中,A 券和 B 券的价值分别为 \(A_i\) 和 \(B_i\),当天比例系数为 \(R_i\)
有两种交易方式:
将手头的所有 A 券和 B 券以当天价格兑换为 rmb
支付所有 rmb (设共 \(v\) 单位),交易所将兑换给用户总价值为 \(v\) 的金券
其中,满足兑换的 A 券数量除以 B 券数量等于当天比例系数
每天可以交易多次,也可以不交易
给定天数 \(n\) 与初始钱数 \(s\),设 \(A_i, B_i, R_i\) 均已知,求 \(n\) 天后能获得的最大钱数
\(n \le 10^5\),\(0 < A_i, B_i \le 10\),\(0 < R_i \le 100\),保证答案 \(\le 10^9\)
与标准答案绝对误差 \(\le 0.001\) 即视为正确;1s, 128MB
考虑 DP,显然无法把金券数记到状态里,考虑枚举连续段转移
具体的,记 \(f_{i, 0/1}\) 表示第 \(i\) 天结束时,手里拿钱 / 金券,最大总价格
由于钱和金券可随时互化,第 \(2\) 维可合并,只需记 \(f_i\) 表示第 \(i\) 天结束时,手里钱的最大价格 / 金券在当天代表的钱的最大价格即可
转移时,枚举 \(f_j\),钦定第 \(j\) 天的金券一直留到了第 \(i\) 天:
-
设第 \(j\) 天买入 \(x\) 单位 B 券与 \(R_j \cdot x\) 单位 A 券,有 \(B_j \cdot x + A_j \cdot R_j \cdot x = f_j\),解得 \(x = \frac{f_j}{A_j \cdot R_j + B_j}\)
因此,第 \(i\) 天卖出的收益为 \(A_i \cdot R_j \cdot x + B_i \cdot x = \frac{R_j \cdot f_j}{A_j \cdot R_j + B_j} A_i + \frac{f_j}{A_j \cdot R_j + B_j} B_i\)
令 \(x_j = \frac{R_j \cdot f_j}{A_j \cdot R_j + B_j}, y_j = \frac{f_j}{A_j \cdot R_j + B_j}\),收益即为 \(x_j \cdot A_i + y_j \cdot B_i\)
-
钦定从 \(j\) 转移比从 \(k\) 转移优,即有 \(x_j \cdot A_i + y_j \cdot B_i \ge x_k \cdot A_i + y_k \cdot B_i\)
移项相除,可得 \(\frac{y_j-y_k}{x_j-x_k} \ge -\frac{A_i}{B_i}\)
这显然为斜率的形式,但 \(x_i, y_i\) 均没有单调性,做不了
-
考虑 cdq 分治,对分治区间 \([l, r]\),我们只关心 \([l, \text{mid}]\) 对 \([\text{mid}+1, r]\) 的贡献,不关心 \([l, \text{mid}]\) 内部的顺序
因此,可以强制令 \([l, \text{mid}]\) 按 \(x_i\) 从小到大排序
按照斜率优化 DP 的套路,容易发现对 \([l, \text{mid}]\) 维护上凸包即可
对 \([\text{mid}+1, r]\) 按 \(-\frac{A_i}{B_i}\) 从大到小排序,双指针找到上凸包中的决策点并转移
-
注意,由于可以不交易,还有一种转移为 \(f_i \leftarrow \max(f_i, f_{i-1})\),在分治区间 \(l = r\) 时 chkmax 即可
时间复杂度 \(O(n \log^2 n)\)
*P5445 [APIO2019] 路灯 (贡献前缀 + 矩形加单点查 - cdq 分治)
数轴上有 \(1, 2, \cdots, n+1\) 共 \(n+1\) 个位置
第 \(0\) 时刻,给定 \(1 \le p_1 < p_2 < \cdots < p_k \le n\),连无向边 \((p_i, p_i+1)\)
接下来 \(q\) 个时刻,每个时刻会发生以下两种事件之一:
- 给定 \(i\),若 \((i, i+1)\) 有边则断开,反之连边 \((i, i+1)\)
- 给定 \(a, b\),求 \(0\) 时刻到当前时刻中,有多少个时刻满足 \(a, b\) 连通
\(1 \le n, q \le 3 \cdot 10^5\);5s, 512MB
[!NOTE]
如何刻画 \(0 \sim i\) 时刻 \(\to\) 贡献前置 \(\to\) 矩形加,单点查,可以 cdq
[!IMPORTANT]
Trick:贡献前置,提前考虑对后面时刻的贡献,多余的后面再减
刻画每个时刻的连通块是容易的,线段树维护连通块左右端点,加 / 删边时分讨,做区间覆盖即可
合并 / 分裂连通块可视为矩形修,查询是单点查
但 \(0 \sim i\) 时刻不好处理,一旦当前贡献包含上个时刻的贡献,就很难搞了
考虑贡献前置,在 \(i\) 时刻把对 \(i+1 \sim q\) 时刻的贡献全加上,矩形修时 \(\pm (q-i)\)
设 \(t\) 时刻询问 \(a, b\),若此时 \(a, b\) 连通,再把 \(q-t\) 减回来 (反之,之前分裂时已经减过了)
这样,每个矩形修独立,将矩形加拆成四个单点加,贡献条件为二维偏序,cdq 维护即可
令 \(n, q\) 同阶,时间复杂度 \(O(n \log^2 n)\)
01 trie
P4735 最大异或和 (转前缀异或和 + 可持久化 01 trie)
给定长为 \(n\) 的非负整数序列 \(a_i\)
有 \(m\) 次操作,每次为以下两种操作类型:
A x,添加操作,表示在序列末尾添加一个数 \(x\),序列长度 \(n\) 加 \(1\)Q l r x,询问操作,找到一个位置 \(p\ (l \le p \le r)\) 使得 \(a_p \oplus a_{p+1} \oplus \cdots \oplus a_n \oplus x\) 最大,输出最大值\(1 \le n, m \le 3 \cdot 10^5\),\(0 \le a_i \le 10^7\);1.5s, 512MB
记 \(s_i\) 为 \(\oplus_{k=1}^{i} a_k\),将 \(\max a_p \oplus \cdots \oplus a_n \oplus x\ (p \in [l, r])\) 转化为 \(\max s_p \oplus s_n \oplus x\ (p \in [l-1, r-1])\)
\(s_n \oplus x\) 为定值,即求定值与区间中值的异或最值,考虑可持久化 01 trie
类似可持久化线段树:
-
对修改操作,将经过的每个点复制 \(1\) 份再操作
-
维护每个点的经过次数 \(\text{cnt}_i\)
对查询操作,仍贪心地向高位不同位移动
设两版本对应节点为 \(l, r\),判断是否有 \(\text{cnt}_r-\text{cnt}_l > 0\) 即可
注意在开头插入 \(0\),处理 \(a_1 \oplus \cdots \oplus a_n \oplus x\ (0 \oplus s_n \oplus x)\) 的情况
时间复杂度 \(O((n+m) \log V)\)
P6018 [Ynoi2010] Fusion tree (01 trie 维护异或和与全局加 1 + 只维护儿子,父亲单独做)
给定一棵 \(n\) 个节点的树,初始第 \(i\) 个点的点权为 \(a_i\)
有 \(m\) 次操作,每次形如以下三种之一:
给定节点 \(u\),将与 \(u\) 距离恰为 \(1\) 的节点点权 \(+1\) (不修改自己)
树上两点间的距离定义为最短路径的边数
给定节点 \(u\) 与值 \(x\),将点 \(u\) 点权 \(-x\)
给定节点 \(u\),询问与 \(u\) 距离恰为 \(1\) 的节点点权异或和
\(1 \le n, m \le 5 \cdot 10^5\),\(0 \le a_i \le 10^5\),保证任意时刻点权非负
2s, 500MB
对邻域修改,经典 trick 是只维护儿子,父亲单独做
在每个点开动态开点 01 trie,维护所有儿子的点权异或和:
-
对操作 \(1\),直接在 \(u\) 的 01 trie 中做全局加
对父亲 \(\text{fa}_u\),单独令 \(a_{\text{fa}_u} \leftarrow a_{\text{fa}_u}+1\)
-
对操作 \(2\),考虑在 \(\text{fa}_u\) 的 01 trie 中删掉 \(a_u\),再加上 \(a_u-x\)
问题是,可能有操作 \(1\) 覆盖 \(u\),我们不知道 \(a_u\) 的真实值
考虑维护 \(\text{tag}_u\) 表示 \(u\) 目前经历的操作 \(1\) 的个数
查询时,\(a_u\) 的真实值即为 \(a_u + \text{tag}_{\text{fa}_u}\)
最后令 \(a_u \leftarrow a_u-x\)
-
对操作 \(3\),直接求 \(u\) 的 01 trie 的全局异或和
对父亲 \(\text{fa}_u\),单独将答案异或上 \(\text{fa}_u\) 的真实点权即可
时间复杂度 \(O(n \log n)\)
*P5283 [十二省联考 2019] 异或粽子 (转前缀异或和 + 01 trie 维护 \(\text{kthmax}\) + 求前 k 大技巧)
P5795 [THUSC 2015] 异或运算 (可持久化 trie 维护定值对区间异或第 k 大)
给定长为 \(n\) 的数列 \(x_i\) 与长为 \(m\) 的数列 \(y_i\),令 \(a_{i, j} = x_i \oplus y_j\)
有 \(q\) 次询问,第 \(i\) 次给定 \(u_i, d_i, l_i, r_i, k_i\),求满足 \(u_i \le p \le d_i, l_i \le q \le r_i\) 的第 \(k\) 大 \(a_{p, q}\)
\(1 \le n \le 1000\),\(1 \le m \le 3 \cdot 10^5\),\(1 \le q \le 500\),\(0 \le x_i, y_i < 2^{31}\);2s, 500MB
[!NOTE]
\(n, m\) 不平衡,二分,枚举 \(x_p\),对 \(y_{[l_i, r_i]}\) check \(\to\) 可持久化 trie 维护 \(\to\) 契合 check 过程,按位确定,少 \(1\) 个 \(\log\)
考虑二分,枚举 \(x_p\),对 \(y_{[l_i, r_i]}\) 求 check,容易用可持久化 trie 维护做到 \(O(m \log V + qn \log^2 V)\)
考虑契合 check 的过程,直接按位确定,维护 trie 上 \(u_i-d_i+1\) 个位置,填 0 时判断是否已经不合法
时间复杂度 \(O(m \log V + qn \log V)\)
*UOJ266 -【清华集训2016】Alice和Bob又在玩游戏 ( trie 维护子树 SG 集合 - 全局异或,合并,查 mex )
给定 \(n\) 个点 \(m\) 条边的森林,每棵树的根为连通块中编号最小的点
Alice 和 Bob 轮流操作 (Alice 先手),每回合选择 \(1\) 个未被删除的点 \(u\),删去 \(u\) 到根链上的所有点,不能操作的人输
我们规定,删除节点不影响父子关系
当 Alice 和 Bob 都按照最优策略行动时,判断 Alice 是否有必胜策略
多测,\(1 \le t \le 10\),\(1 \le n \le 10^5\),保证 \(\sum n \le 2 \cdot 10^5\),每棵树的大小 \(\le 5 \cdot 10^4\);2s, 1GB
[!NOTE]
复杂博弈,考虑 SG 函数,维护子树 SG 值 \(\to\) 考虑合并儿子信息,将儿子到根链拼上自己 \(\to\) 全局异或,集合合并,查 \(\text{mex}\),trie 维护
[!IMPORTANT]
Trick1:01 trie 维护 SG 值
Trick2:刻画删到根链 \(\to\) 将儿子到根链拼上自己,合并其他子树信息
复杂博弈,考虑 SG 函数,只需维护每个子树的 SG 值
对删到根链,考虑将儿子到根链拼上自己,把其他子树的信息合并上来
维护每个子树的后继状态 SG 值集合,记为 \(S_u\),设子树 \(u\) 的 SG 值为 \(w_u\)
当 \(u\) 拼上儿子 \(v_0\) 的到根链, 对 \(S_{v_0}\) 的影响是整体异或上 \(\oplus_{v \in \text{son}_u \land v \ne v_0} w_v\),再合并到 \(S_u\) 中
同时,也可以选择只删 \(u\),即在 \(S_u\) 中插入 \(\oplus_{v \in \text{son}_u} w_v\)
综上,需要支持全局异或 + 集合合并 + 插入 + 查 \(\text{mex}\),01 trie 维护即可:
- 对全局异或,在每个点打标记,下传时判断是否需要 swap 两儿子
- 对合并,类似线段树合并,无值返回 + 有值递归合并后 pushup
- 对查 \(\text{mex}\),在每个点维护子树内叶子个数 (不重复计数),查询时左子树满了再走右子树
时间复杂度 \(O(n \log V)\)
线段树分治
总结:
-
常见问题形式:
- 维护撤销操作
- 修改 / 询问只对一段时间区间造成影响
-
要求:
- 可以在时间维拆分修改区间 / 询问区间
- 静态问题可做,支持末尾撤销 (部分,常见可撤销并查集)
-
Trick:
- 修改为时间区间,询问为时间点:线段树维护修改时间区间,钦定儿子继承父亲状态
- 询问为时间区间,修改为时间点:线段树维护询问时间区间,修改复制 log 份挂在到根链上,无需继承状态
- 时间区间左 / 右端点不确定,考虑按特殊顺序遍历线段树 (先左后右 / 先右后左),边 solve 边 update
LOJ121 「离线可过」动态图连通性 (线段树分治 + 可撤销并查集)
维护 \(n\) 个点的无向连通图,初始无边,有 \(m\) 次操作:
- 加入 \(1\) 条边,保证这条边不存在
- 删除 \(1\) 条边,保证这条边存在
- 给定 \(u, v\),查询 \(u, v\) 是否连通
\(n \le 5000\),\(m \le 5 \cdot 10^5\);800ms, 512MB
考虑加入时间维,求出每个加入操作 \(i\) 在时间维上的存在区间 \([l_i, r_i]\)
考虑朴素暴力,对每个时间开桶,将加入操作 \(i\) 插入桶 \([l_i, r_i]\) 中
对时间 \(t\),并查集维护桶 \(t\) 中的所有加入操作 + 查询连通性即可
本质上,我们加入时间维,用 \(O(m)\) 的代价规避了 \(1\) 次撤销操作
考虑线段树维护这个过程,线段树上每个节点代表 \(1\) 个时间区间:
-
对插入操作 \(i\),将 \([l_i, r_i]\) 拆分为线段树上 \(O(\log m)\) 个区间
-
对时间 \(t\) 的查询操作,找到线段树上代表 \([t, t]\) 的叶子 \(\text{leaf}\)
只需维护 \(\text{leaf}\) 到根链上的所有加入操作,即可得到 \(t\) 时刻的状态
-
遍历线段树区间,钦定儿子继承父亲的状态,设当前区间为 \([l, r]\):
- 全局并查集维护 \([l, r]\) 的加入操作
- 递归 \([l, \text{mid}]\),回溯后撤销 \([l, \text{mid}]\) 中加入操作的影响
- 递归 \([\text{mid}+1, r]\),回溯后撤销 \([\text{mid}+1, r]\) 中加入操作的影响
- 在叶子处处理查询
实现上需要可撤销并查集:
- 只能按秩合并
- 开全局栈记录 merge 操作,撤销时弹栈 + 复原
时间复杂度 \(O(m \log m \log n)\)
*P5787 【模板】线段树分治 / 二分图 (线段树分治 + 扩展域可撤销并查集)
有一个 \(n\) 个节点的图,\(k\) 时间内,有 \(m\) 条边会出现后消失
具体的,第 \(i\) 条边连接 \(x_i, y_i\),在 \(l_i\) 时刻出现,\(r_i\) 时刻消失
对于所有 \(k\) 个时间段,求第 \(i\) 时间段内这个图是否为二分图
\(n, k = 10^5\),\(m = 2 \cdot 10^5\),\(0 \le l \le r \le k\);1s, 256MB
上线段树分治,只需维护加边 + 判断二分图
考虑扩展域并查集,将每个点 \(i\) 拆成两点 \(i, n+i\),分别表示二分图两集合中的点
连边 \(u, v\) 时:
- 在并查集中合并 \(u, n+v\) 与 \(n+u, v\)
- 若 \(u, n+u\) 在同一集合 / \(v, n+v\) 在同一集合,说明不合法
回溯时维护并查集撤销即可,时间复杂度 \(O(k \log k \log n)\)
P4735 最大异或和 (转前缀异或和 + 可持久化 01 trie)
给定长为 \(n\) 的非负整数序列 \(a_i\)
有 \(m\) 次操作,每次为以下两种操作类型:
A x,添加操作,表示在序列末尾添加一个数 \(x\),序列长度 \(n\) 加 \(1\)Q l r x,询问操作,找到一个位置 \(p\ (l \le p \le r)\) 使得 \(a_p \oplus a_{p+1} \oplus \cdots \oplus a_n \oplus x\) 最大,输出最大值\(1 \le n, m \le 3 \cdot 10^5\),\(0 \le a_i \le 10^7\);1.5s, 512MB
记 \(s_i\) 为 \(\oplus_{k=1}^{i} a_k\),将 \(\max a_p \oplus \cdots \oplus a_n \oplus x\ (p \in [l, r])\) 转化为 \(\max s_p \oplus s_n \oplus x\ (p \in [l-1, r-1])\)
\(s_n \oplus x\) 为定值,即求定值与区间中值的异或最值,考虑可持久化 01 trie
类似可持久化线段树:
-
对修改操作,将经过的每个点复制 \(1\) 份再操作
-
维护每个点的经过次数 \(\text{cnt}_i\)
对查询操作,仍贪心地向高位不同位移动
设两版本对应节点为 \(l, r\),判断是否有 \(\text{cnt}_r-\text{cnt}_l > 0\) 即可
注意在开头插入 \(0\),处理 \(a_1 \oplus \cdots \oplus a_n \oplus x\ (0 \oplus s_n \oplus x)\) 的情况
时间复杂度 \(O((n+m) \log V)\)
P4585 [FJOI2015] 火星商店问题 (线段树分治 - 询问带时间区间,修改复制 log 份 + 可持久化 01 trie)
有 \(n\) 个商店,每个商店每天可能进新商品,每个商品有 \(1\) 个非负整数标价 \(v_i\)
第 \(i\) 个商店有 \(1\) 种特殊商品 \(a_i\),其不受进货日期限制,任何时候都有
若干天中有 \(m\) 次事件:
0 s v,表示编号为 \(s\) 的商店在当日新进 \(1\) 种标价为 \(v\) 的商品
1 l r x d,设当前为第 \(t\) 天,表示询问 \([l, r]\) 内的商店 \([t-d+1, t]\) 天内的商品中,标价 \(\oplus\ x\) 的最大值特殊地,若 \(d = 0\),则只考虑 \([l, r]\) 内商店的特殊商品
我们钦定,当出现 \(0\) 操作时代表开始新的一天
对于操作序列开头连续的 \(1\) 操作,默认只需考虑特殊商品
保证输入的所有数据 \(\in [0, 10^5]\);2s, 500MB
本题询问带有时间区间 \([t-d+1, t]\),修改只在单个时间点进行
考虑线段树分治:
-
对每个询问,拆成 \(O(\log m)\) 个区间挂在线段树上
-
对每个修改,复制 \(O(\log m)\) 份挂在到根链中的每个节点上
-
当处理线段树上区间 \([l, r]\) 时,先处理挂在该点上的修改操作,再处理对挂在该点上的查询的贡献
容易发现,此时挂在 \([l, r]\) 的修改操作即为所有真实修改操作,无需令子节点继承当前节点状态
因此,也无需考虑撤销问题
考虑如何维护修改 + 统计查询贡献:
- 对每个修改按 \(s\) 排序,建可持久化 trie
- 对每个查询,二分对其有贡献的修改区间 \([l, r]\),可持久化 trie 上查 \([l, r]\) 与 \(x\) 的异或 \(\max\) 即可
对特殊商品,单独建可持久化 trie,初始统计对所有询问的贡献即可
时间复杂度 \(O(m \log m \log V)\)
CF576E - Painting Edges (线段树分治 - 时间区间右端点未知,拆段,边 solve 边 update)
给定 \(n\) 个节点 \(m\) 条边的无向图,每条边可以未染色,或被染成 \(k\) 种颜色之一
最初,所有边都未染色
有 \(q\) 次操作,每次形如 "将边 \(e_i\) 重新染成 \(c_i\) ";任何时刻,对同一种颜色形成的子图,必须是二分图
若某次重染操作使该颜色的子图不是二分图,则该操作不合法,边 \(e_i\) 保持原有颜色,否则执行染色
对每次重染操作,判断此次操作是否合法
\(2 \le n \le 5 \cdot 10^5\),\(1 \le m, q \le 5 \cdot 10^5\),\(1 \le k \le 50\);6s, 600MB
若已知边 \(e_i\) 颜色为 \(\text{col}\) 的时间段 \([l, r]\),可以直接上线段树分治,对每种颜色开扩展域并查集
问题在于,我们不知道修改操作是否会真的修改,没法求出 \(r\)
设对边 \(e_i\) 重染的时间点为 \(t_1, t_2, \cdots, t_k\):
- 显然,将同颜色时间段 \([l, r]\) 拆成若干小区间 \([l, r_1'], [r_1'+1, r_2'], \cdots, [r_k', r]\) 不影响答案
- 以 \(t_1, t_2\) 为例:
- 注意到,线段树分治 \([1, t_1]\) 时,是不会经过 \([t_1+1, q]\) 的,因此就算后面的信息未知也没关系
- 考虑在 \([t_1, t_1]\) 判断修改 \(t_1\) 是否执行,更新答案,并将 \([t_1+1, t_2-1]\) 及对应颜色
update进线段树
实现上,可以维护每条边最近的颜色 \(\text{lst}_i\)
显然,形如 \([t_1, t_1]\) 修改的判断 + \(\text{lst}_i\) 的更新只需在叶子处执行,每个叶子恰好 \(1\) 次修改操作 (对应询问序列)
时间复杂度 \(O(q \log q \log n)\)
**CF603E - Pastoral Oddities (刻画答案 - 连通块大小为偶 + 类瓶颈生成树 + 线段树分治 - 从后向前求区间终点)
初始有 \(n\) 个点,其间没有任何边
现计划修建 \(m\) 条无向路径,第 \(i\) 条连接点 \(u_i, v_i\),长度为 \(w_i\)
对每个 \(i\ (1 \le i \le m)\),你需要在前 \(i\) 条路径中选若干条真正连上,满足:
- 每个点的度数均为奇数
- 在所有合法选择方案中,该方案最大边权最小
对每个 \(i\ (1 \le i \le m)\),求前 \(i\) 条路径最优方案的最大边权,无解输出
-1\(2 \le n \le 10^5\),\(1 \le m \le 3 \cdot 10^5\),\(1 \le w_i \le 10^9\);4s, 250MB
考虑如何刻画答案:
-
显然,最优方案中不会有环;反之,将环删去,对每个点的度数奇偶性没有影响,必然不劣
-
设点 \(i\) 度数为 \(\text{deg}_i\),第 \(i\) 个连通块点集为 \(S_i\),大小为 \(\text{siz}_i\)
显然,有 \(\sum_{k \in S_i} \text{deg}_k = 2 \cdot \text{siz}_i\)
要求 \(\text{deg}_k\) 均为奇数,则必有 \(\text{siz}_i\) 为偶数
-
若 \(\text{siz}_i\) 均为偶数,则必能通过如下方式删去若干条边,构造出合法解:
- 拉出每个连通块,先将环删去
- 对剩下的树结构,按深度从大到小考虑每个点,若儿子数为奇则删去父边
- 由度数和奇偶性可知,根最终必然满足要求,方案合法
考虑瓶颈生成树的解决方式:
- 按边权从小到大加入每条边,直到不存在度数和为奇的连通块,此时该边权就是答案
- 由上述性质,此时必能删去若干条边构造出合法解;若能删去当前边,则之前就能得到答案
对静态问题,将边权排序后,并查集维护度数和为奇的连通块个数即可
考虑动态问题:
- 显然,若边 \(i\) 在其出现时不在最优解集合中,它以后也不会出现
- 若边 \(i\) 在边 \(j\ (j > i)\) 加入后被顶出最优解集合,它以后也回不来了
因此,在最优解集合中,每条边的出现时刻为一段区间;这启示我们线段树分治
显然,第 \(i\) 条边的时间区间左端点为 \(i\),考虑如何确定右端点:
-
考虑时光倒流,从后向前做
对所有边按边权从小到大排序,在叶子 \([t, t]\),依次考虑所有边:
-
若该边的原序列位置 \(>t\),说明是后面的边,没用
-
反之,将其加入并查集,若此时奇连通块个数减为 \(0\) 则结束
可以视为,后面的边都被踢掉了,在 \([t, m]\) 无法加入最优解集合
对能加入的边,设其原序列位置为 \(i\),则其时间区间已确定,为 \([i, m]\)
对 \([m, m]\) 当前直接加入,\([i, m-1]\) 在线段树上
update维护 -
显然,每条边只会被确定 \(1\) 次,可以直接维护边序列上的指针
-
时间复杂度 \(O(m \log m \log n)\)
P3733 [HAOI2017] 八纵八横 (拉出生成树做 + 线段树分治 + 线性基 - bitset 维护)
整体二分
总结:
-
常见问题形式:多次二分,对每个单独 check 无法接受
-
要求:答案有可二分性
-
Trick:
-
复杂度正确性要求:带答案值域 \([l, r]\) 一起分治,当前层 check 只加入 \([l, \text{mid}]\) 的相关信息
因此,需要注意如何让 \([\text{mid}+1, r]\) 继承 \([l, \text{mid}]\) 的信息
常用策略:
- 若询问要求可差分,则直接差分
- 保留 \([l, \text{mid}]\) 的半成品信息,先对 \([\text{mid}+1, r]\) 做,撤掉半成品信息后再对 \([l, \text{mid}]\) 做
-
若带修,可带着修改一起二分,只需保持相对前后关系不变即可
-
多组询问,原问题不好做,但判定问题会做,可二分转判定 + 套整体二分
-
P1527 [国家集训队] 矩阵乘法 (整体二分)
给定 \(n \times n\) 的矩形 \(a_{i, j}\),有 \(q\) 次询问:
- 给定 \(x_1, y_1, x_2, y_2, k\),查询以 \((x_1, y_1)\) 为左上角,\((x_2, y_2)\) 为右下角的子矩形中第 \(k\) 小数
\(1 \le n \le 500\),\(1 \le q \le 6 \cdot 10^4\),\(0 \le a_{i, j} \le 10^9\);1.5s, 120MB
整体二分的思想为,最大化不同询问间判定的重复部分,以此优化复杂度
令二分值域为 \([l, r]\),询问区间为 \([\text{ql}, \text{qr}]\) (重标号),solve(l, r, ql, qr) 的流程为:
-
将 \(a_{i, j} \in [l, \text{mid}]\) 的位置在二维树状数组中置 \(1\)
-
枚举询问 \(q = \{x_1, y_1, x_2, y_2, k\}\),在二维树状数组上查询 \(\{x_1, y_1, x_2, y_2\}\) 子矩形中 \(1\) 的个数 \(w\):
-
若 \(w \ge k\),说明 \(\text{mid}\) 大了,归为第 \(1\) 类
-
若 \(w < k\),说明 \(\text{mid}\) 小了,令 \(k \leftarrow k-w\),归为第 \(2\) 类
-
-
对两类分别递归求解即可
时间复杂度分析:
- 二分最多 \(O(\log n)\) 层
- 对每层,每个值最多加入二维树状数组 \(1\) 次,这部分复杂度 \(O(n^2 \log^2 n)\)
- 对每层,每个查询最多处理 \(1\) 次,这部分复杂度 \(O(q \log^2 n)\)
综上,总复杂度 \(O(n^2 \log^3 n + q \log^3 n)\)
P4175 [CTSC2008] 网络管理 (整体二分 - 带着修改一块二分)
给定 \(n\) 个节点的树,每个点初始点权为 \(t_i\),有 \(q\) 次操作:
给定 \(u, w\),表示 \(t_u\) 变为 \(w\)
给定 \(u, v, k\),查询 \(u, v\) 间路径点集的第 \(k\) 大点权
特别的,若路径点集大小 \(<k\),则输出
invalid request!\(1 \le n, q \le 80000\),\(0 \le k \le n\),\(V \le 10^8\);2s, 500MB
将初始点权看作加入 ( \(+1\) ),将修改拆成删除 ( \(-1\) ) 后加入 ( \(+1\) )
去掉修改后就是整体二分板子,考虑带修怎么做
事实上,可以直接带着修改一起二分,具体的:
-
设分治值域区间为 \([l, r]\),操作区间为 \([\text{ql}, \text{qr}]\)
对修改操作 \(\{u_i, w_i\}\ (i \in [\text{ql}, \text{qr}])\),若 \(w_i \le \text{mid}\) 则将其下传至 \([l, \text{mid}]\),反之下传至 \([\text{mid}+1, r]\)
注意,处理当前区间查询 + 下传时需保证修改与查询的前后关系不变
实现上:
- 单点加 + 路径查可转化为到根链加 + 树上差分,即子树加 + 单点查,可以树状数组
- 本题求第 \(k\) 大,处理当前区间查询时求路径上 \(\le \text{mid}\) 的数会很诡异,可以直接求 \(\ge \text{mid}+1\) 的数
时间复杂度 \(O(n \log^2 n + q \log^2 n)\)
P3527 [POI 2011] MET-Meteors (对时间维整体二分)
有一个点数为 \(m\) 的环,其中 \(i, i+1\) 相邻, \(m, 1\) 相邻
有 \(n\) 个集合,点 \(i\) 属于集合 \(o_i\);初始所有点点权为 \(0\),有 \(k\) 个事件:
- 给定 \(l_i, r_i, a_i\),若 \(l_i \le r_i\),将 \(l_i, l_i+1, \cdots, r_i\) 的点权 \(+a_i\),反之将 \(l_i, l_i+1, \cdots, m, 1, 2, \cdots, r_i\) 的点权 \(+a_i\)
给定集合 \(i\) 的限制 \(p_i\),对所有 \(i\),求出最早第几次事件后集合 \(i\) 内点权和 \(\ge p_i\);若 \(k\) 次事件后仍 \(<p_i\),输出
NIE\(1 \le n, m, k \le 3 \cdot 10^5\),\(1 \le p_i, a_i \le 10^9\)
考虑对时间维整体二分,树状数组维护区间加 + 单点查即可 (总点数为 \(m\),对每个集合暴力查就是对的)
时间复杂度 \(O(n \log^2 m + m \log^2 m)\)
**Gym102354B - Yet Another Convolution (拆绝对值 - 做两次 + 莫反 + 二分转判定 + 整体二分)
给定两个长为 \(n\) 的整数序列 \(a_i, b_i\),求序列 \(c_i\),满足:
- \(c_k = \max\limits_{\gcd(i, j) = k} |a_i-b_j|\)
\(1 \le n \le 10^5\),\(1 \le a_i, b_i \le 10^9\);6s, 256MB
考虑拆绝对值,对 \((a_i-b_j)\) 与 \((b_j-a_i)\) 做两次即可
令 \(b_j\) 取反,转化为对每个 \(k\) 求 \(c_k = \max\limits_{\gcd(i, j) = k} (a_i+b_j)\)
转化下答案:
令 \(a_i' = a_{ik},\ b_i' = b_{ik},\ m = \lfloor \frac{n}{k} \rfloor\),所求形如 \(\max\limits_{i=1}^{m}(a_i + \max\limits_{j=1}^{m} b_j\ [\gcd(i, j) = 1])\)
转化为给定 \(m\) 个 \(i\),希望快速求出 \(\max\limits_{j=1}^{m} b_j\ [\gcd(i, j) = 1]\)
\(\max\) 的形式很难受,考虑二分答案,转化为判定问题;这样的好处是,我们可以上莫反,将 \(i, j\) 的式子分开
设二分答案为 \(\text{mid}\),答案 \(\ge \text{mid}\) 的判定条件为:
对 \(m\) 组询问,考虑整体二分,暴力 \(O(d(i))\) 修改,\(O(1)\) 查询维护 \(\sum\limits_{d|j}\ [\text{mid} \le b_j]\)
二分至 \(\text{mid}\) 时,若 \(b_j \ge \text{mid}\) 则对 \(j\) 修改,最终对每个询问 \(O(d(i))\) 查询判定即可
分析下复杂度:
- 二分最多 \(O(\log m)\) 层
- 对每层,每个值最多修改 \(1\) 次,单层时间复杂度 \(O(\sum\limits_{i=1}^{m} d(i)) = O(m \log m)\)
- 对每层,每个询问最多查询 \(1\) 次,单层时间复杂度 \(O(\sum\limits_{i=1}^{m} d(i)) = O(m \log m)\)
- 对 \(m\),整体二分复杂度为 \(O(m \log^2 m)\)
- 总复杂度 \(O(\sum\limits_{i=1}^{n} \lfloor \frac{n}{i} \rfloor \log^2 \lfloor \frac{n}{i} \rfloor) = O(n \log^3 n)\)
**P5163 WD与地图 (有向转无向 - 整体二分每条边两侧 scc 合并时间 + 代表元 + 动态开点权值线段树合并)
给定 \(n\) 个点,\(m\) 条边的有向图,第 \(i\) 个点的初始点权为 \(s_i\)
有 \(q\) 次操作:
- 给定 \(u, v\),删去边 \(u \to v\),保证这条边存在
- 给定点 \(u\) 与值 \(w\),令 \(s_u \leftarrow s_u+w\)
- 给定点 \(u\) 与参数 \(k\),查询 \(u\) 所在 scc 前 \(k\) 大点权和
\(n \le 10^5\),\(m \le 2 \cdot 10^5\),\(q \le 2 \cdot 10^5\);3s, 500MB
以下认为 \(n, m, q\) 同阶
首先,时光倒流把删边改为加边
如果把有向变成无向,我们是会做的
并查集维护连通性,对每个连通块开动态开点权值线段树,合并连通块时做线段树合并,修改 + 查询直接做
这部分时间复杂度 \(O(n \log V)\)
考虑如何把有向变成无向
对每条边,我们希望确定其两侧 scc 合并的时间 \(t\),\([t, +\infty]\) 内这条边即可看成无向边
注意到答案可二分,有 \(O(n)\) 次询问,考虑对时间维整体二分
对分治区间 \([l, r]\) 与询问区间 \([\text{ql}, \text{qr}]\):
-
朴素暴力为直接连上加边时间在 \([1, \text{mid}]\) 的有向边,跑 tarjan 缩点
对 \([\text{ql}, \text{qr}]\) 间的询问,按每条边两侧是否在同个 scc 内分组,分治下去求解
-
为保证复杂度,我们只能加入加边时间在 \([l, \text{mid}]\) 的边 (*);问题是,如何刻画 \([1, \text{mid}]\) 的连通性对 \([\text{mid}+1, r]\) 的影响
考虑留下 \([1, \text{mid}]\) 的半成品图,分治求解 \([\text{mid+1}, r]\) 时直接在上面跑 tarjan,具体的:
-
对半成品图中 scc 的关系,考虑并查集将 \(1\) 个 scc 内的点全缩到一块
-
由分组条件,\([\text{ql}, \text{qr}]\) 中分到 \([l, \text{mid}]\) 的边必然能缩,缩边关系在并查集上已经体现,半成品图上无需保留这些边
注意,\([\text{ql}, \text{qr}]\) 中分到 \([l, \text{mid}]\) 的边 \(\ne\) 所有加边时间在 \([l, \text{mid}]\) 的边
\([\text{ql}, \text{qr}]\) 中有些边加入后最终不在 scc 中,它们的加边时间 \(\le \text{mid}\),但会被分到 \([\text{mid}+1, r]\),在递归右侧时也应该被加上
(*) 因此,前面说的不太准确,我们加的实际上是 \([\text{ql}, \text{qr}]\) 中加边时间 \(\le \text{mid}\) 的边
二分的询问边就是修改边边集,答案值域只是对这些边的加边时间进行了限制
-
为适配半成品图,需要修改加边定义,加边 \((u, v)\) 时实际上连的是 \((\text{gf}(u), \text{gf}(v))\) (并查集上祖先)
-
跑 tarjan 时,维护非孤点集合 \(S\),只对 \(S\) 中的点 tarjan,保证复杂度
初始半成品图内全是孤点,只有连边的点可能被缩起来,正确性没问题
综上,流程为:
- 连 \([\text{ql}, \text{qr}]\) 中加边时间 \(\le \text{mid}\) 的边,并查集维护半成品图上的 scc
- 对 \([\text{mid}+1, r]\) 分治下去求解
- 撤销并查集上的关系
- 对 \([l, \text{mid}]\) 分治下去求解
最多分治 \(O(\log n)\) 层,层内每条边最多加 \(1\) 次,总时间复杂度 \(O(n \log^2 n)\)
-
-
真的需要可撤销并查集么?
本质上,我们只需对每个 scc 找出 \(1\) 个代表元,以后连边都用代表元连 (上述 \((\text{gf}(u), \text{gf}(v))\) 实际上就是在干这个)
若代表元被缩起来,自然代表 scc 内的所有点都被缩起来
综上,新流程为:
-
连 \([\text{ql}, \text{qr}]\) 中加边时间 \(\le \text{mid}\) 的边,tarjan 时确定代表元
-
对 \([\text{ql}, \text{qr}]\) 中被分到 \([\text{mid}+1, r]\) 的边,用代表元将两端点重标号
-
对 \([l, \text{mid}]\) 分治下去求解
-
对 \([\text{mid}+1, r]\) 分治下去求解
时间复杂度 \(O(n \log n)\)
-
综上,总复杂度 \(O(n \log V)\)
最值分治 / 笛卡尔树分治
*P4755 Beautiful Pair (最值分治)
给定长为 \(n\) 的序列 \(a_i\),求满足如下条件的数对 \((i, j)\ (i \le j)\) 个数:
- \(a_i \cdot a_j \le \max(a_i, a_{i+1}, \cdots, a_j)\)
\(1 \le n \le 10^5\),\(1 \le a_i \le 10^9\)
考虑最值分治,设当前分治区间为 \([l, r]\):
-
st 表维护 \([l, r]\) 的 \(\max\),设其在 \(\text{mid}\) 处取到
-
启发式合并思想,遍历 \([l, \text{mid}]\) 与 \([\text{mid}+1, r]\) 中较短的一侧,则需在另一侧统计 \(\le x\) 的元素个数
离散化后上主席树维护即可
-
递归 \([l, \text{mid}]\) 与 \([\text{mid+1}, r]\),\(l = r\) 时特判 \(a_l = 1\) 即可
考虑分析复杂度:
-
对位置 \(i\),倒数第 \(1\) 次遍历其时区间长度 \(\ge 1\),倒数第 \(2\) 次时 \(\ge 2\)……
显然,位置 \(i\) 被遍历的次数上界是 \(O(\log n)\) 的
-
每次求贡献是 \(O(\log n)\) 的,总复杂度 \(O(n \log^2 n)\)
实现上,注意到求出以当前元素为 \(\max\) 的极长区间后,对两侧做如上启发式操作与分治本质相同,直接做即可
**P5044 [IOI 2018] meetings 会议 (最值分治 - 笛卡尔树上 DP + 分析变化量,二分分界点 + 线段树优化 DP)
给定长为 \(i\) 的数列 \(h_i\),有 \(q\) 次询问,每次给定 \(l, r\):
你需要选择位置 \(k\),要求 \(l \le k \le r\)
对位置 \(j\),在选择 \(k\) 时,代价 \(f_k(j)\) 定义为 \(j, k\) 间所有 \(h_i\) 的 \(\max\)
定义选择 \(k\) 的代价 \(F(k) = \sum_{l \le j \le r} f_k(j)\)
求 \(\min_{l \le k \le r} F(k)\)
\(1 \le n, q \le 7.5 \cdot 10^5\),\(1 \le h_i \le 10^9\);4.5s, 805MB
[!NOTE]
贡献为最值 \(\to\) 最值分治,这样另 \(1\) 侧贡献确定 \(\to\) 笛卡尔树上 DP \(\to\) 观察性质,转移式变化量单调,二分分界点 \(\to\) 线段树优化 DP
[!IMPORTANT]
Trick:贡献为一万个区间最值,考虑最值分治 + 笛卡尔树上 DP
Trick:对两式取 \(\min\) / \(\max\) 的转移式,注意分界点是否唯一
看到一大堆区间最值,考虑最值分治
设询问区间为 \([l, r]\),令 \(w = \max_{l \le k \le r} h_k\),在 \(k_0\) 处取到
分治 \([l, k_0-1]\) 和 \([k_0+1, r]\),这么干的好处是,分治 \(1\) 侧后,另 \(1\) 侧贡献全是 \(w\),是确定的
注意到,这就是笛卡尔树的结构
对笛卡尔树上点 \(u\),设 \([l_u, r_u]\) 为其管辖区间
观察分治区间的性质,容易发现,对分治区间 \([l, r]\),总能找到 \(u\),使 \(l_u = l\) 或 \(r_u = r\)
考虑在笛卡尔树上 DP,对每个点 \(u\),处理出以 \(l_u\) 为左端点 / \(r_u\) 为右端点的 DP 值;两种情况本质相同,以下只考虑左端点
令 \(f_{l_u, i}\) 表示 \([l_u, i]\) 的答案,递归求解 \(u\) 左右儿子后,\(f_{l_u, [l_u, u-1]}\) 与 \(f_{u+1, [u+1, r_u]}\) 已确定,同时有 \(f_{l_u, u} = f_{l_u, u-1}+h_u\)
有转移 \(f_{l_u, i} \leftarrow \min(f_{l_u, u-1} + (i-u+1)h_u, f_{u+1, i}+(u-l+1)h_u)\),表示分讨选在哪边
Key Observation:\(i\) 增大时,左式 \(\varDelta = h_u\),右式 \(\varDelta \le h_u\),分界点唯一确定
设分界点为 \(\text{mid}\),则 \(1\) 侧是区间加一次函数,\(1\) 侧是区间加
这启示我们对第二维建线段树,做线段树优化 DP,可以线段树上二分求分界点,顺便更新 DP 值:
- 若当前区间被询问区间完全包含,分界点在区间中则递归下去,反之做区间修
- 在线段树上区间 \([l, r]\),维护 \(l, r\) 的 DP 值以快速判断分解点
令 \(n, q\) 同阶,时间复杂度 \(O(n \log n)\)
猫树分治
总结:
-
常见问题形式:合并信息复杂度较高 + 前后缀合并可以接受 / 固定区间起或终点简化问题
-
要求:答案可合并
-
Trick:
-
按是否跨过 \(\text{mid}\) 分类,处理出 \([l \sim \text{mid}, \text{mid}]\) 的前缀信息 + \([\text{mid}+1, \text{mid}+1 \sim r]\) 的后缀信息
查询 \([l', r']\) 时,合并 \([l', \text{mid}]\) 与 \([\text{mid}+1, r']\) 的信息即可
-
固定左 / 右端点为 \(\text{mid}\),使查询区间左 / 右端点相同,快速撤销信息
-
P6240 好吃的题目 (猫树分治 - 维护 01 背包)
有 \(n\) 个物品,第 \(i\) 个物品体积为 \(w_i\),价值为 \(v_i\)
\(m\) 次询问,每次给定 \(l_i, r_i, t_i\),求拉出 \([l_i, r_i]\) 内的物品跑容量为 \(t_i\) 的 01 背包的价值 \(\max\)
\(1 \le n \le 4 \cdot 10^4\),\(1 \le m \le 2 \cdot 10^5\),\(1 \le h_i, t_i \le 200\),\(1 \le w_i \le 10^7\);2s, 125MB
先介绍猫树:
-
猫树是一种特殊的线段树,建树 \(O(n \log n)\) 次信息合并,查询 \(O(1)\) 次信息合并,不支持修改
-
对查询区间 \([l, r]\),拉出线段树上两叶子 \([l, l], [r, r]\),考察它们的 \(\text{lca}\),记为 \([L, R]\)
容易发现,\([L, R]\) 必完全包含 \([l, r]\),且 \([l, r]\) 跨过 \([L, R]\) 的中点
-
对线段树上区间 \([l, r]\),建树时预处理 \([l, \text{mid}]\) 的后缀和 + \([\text{mid}+1, r]\) 的前缀和
这部分有 \(O(n \log n)\) 次信息合并
-
查询 \([l, r]\) 时,找到 \(\text{lca}\),利用前缀和 + 后缀和 \(O(1)\) 次信息合并求出答案即可
-
现在瓶颈在于求 \(\text{lca}\);考虑将序列长度补齐为 \(2^k\),有性质:
-
\(\text{lca}(u, v)\) 就是 \(u, v\) 二进制下的 \(\text{lcp}\) (显然为 \(u, v\) 祖先,由后 \(1\) 位不同,可知 \(u, v\) 在其不同子树中)
-
记 \(f(x) = \lfloor \log_2(x) \rfloor + 1\) 表示 \(x\) 二进制下位数,有 \(\text{lcp}(u, v)\) 为 \(u\) 右移 \(f(u \oplus v)\) 位得到的数
证:\(u \oplus v\) 后 \(\text{lcp}\) 全变 \(0\),后面全变 \(1\),\(\text{lcp}\) 即原数扔掉 \(u \oplus v\) 中的 \(1\) 位
-
-
预处理 \(f(x)\) 即可做到查询 \(O(1)\) 次信息合并
猫树分治的思想类似, 只不过没有显式建出猫树
将询问离线,设分治区间为 \([l, r]\):
- 仍旧预处理 \([l, \text{mid}]\) 的后缀和 + \([\text{mid}+1, r]\) 的前缀和
- 遍历查询 \([l', r']\):
- 若 \(r' \le \text{mid}\),分治 \([l, \text{mid}]\) 时求解
- 若 \(l' \ge \text{mid}+1\),分治 \([\text{mid}+1, r]\) 时求解
- 反之,\([l', r']\) 跨过 \(\text{mid}\),利用前缀和 + 后缀和 \(O(1)\) 次信息合并得到答案即可
实现上,划分询问的方式与整体二分类似
对本题,预处理前缀背包 + 后缀背包,查询时 \(O(V)\) 求合并背包后容量 \(t_i\) 的值即可
时间复杂度 \(O(Vn \log n + mV)\)
CF1100F - Ivan and Burgers (猫树分治 - 维护线性基)
给定长为 \(n\) 的非负整数序列 \(c_i\)
\(q\) 次询问,每次给定 \(l_i, r_i\),求拉出若干 \([l_i, r_i]\) 内的数能异或出来的 \(\max\) 是多少
\(1 \le n, q \le 5 \cdot 10^5\),\(0 \le c_i \le 10^6\);3s, 250MB
上猫树分治,预处理前缀和 + 后缀和时只需往线性基中插入元素,单次 \(O(\log V)\)
合并时,暴力将一个线性基中的数全部插入另一个,单次 \(O(\log^2 V)\)
时间复杂度 \(O(n \log n \log V + q \log^2 V)\)
以下是线性基复习部分:
-
数列 \(a_i\) 的线性基是一个数集 \(S\),满足 \(a_i\) 中任一数都可以通过 \(S\) 的一个子集 (可空) 异或得到,且 \(|S|\) 最小
-
线性基可表示为 \(\lfloor \log V \rfloor\) 个数 \(p_0, p_1, \cdots, p_k\),满足 \(p_i = 0\) 或 \(p_i\) 的二进制最高位为 \(i\)
性质 1:不存在 \(S\) 的非空子集异或和为 \(0\)
证明:反之,其中有 \(1\) 个元素无用,可以删去
性质 2:设线性基大小为 \(k\),其能异或出 \(2^k\) 个不同的数
证明:反之,说明存在两 \(p_i\) 的不同集合 \(S, T\) 的异或和相同,将 \(S\) 和 \(T\) 里的元素异或起来为 \(0\),违反性质 \(1\)
-
设当前要插入 \(v\),构造方式为:
- 按位从高到低考虑,若 \(v\) 当前位为 \(0\) 则跳过
- 反之,若 \(p_i = 0\) 则令 \(p_i \leftarrow v\) 并终止,否则令 \(v \leftarrow v \oplus p_i\)
记从可重集 \(x\) 中选若干个数异或起来得到的数集为 \(T_x\),\(v \oplus x\) 表示值 \(v\) 与 \(x\) 中的数全部异或一遍得到的数
对插入 \(v\) 的操作,若最终 \(v = 0\) 记为 "未成功插入",反之记为 "成功插入"
性质 3:\(T_{\{a_i\}} = T_S\)
证明:归纳,设原线性基为 \(S\),插入 \(v\) 前合法;令插入 \(v\) 时执行操作 \(v \leftarrow v \oplus p_i\) 的 \(p_i\) 构成的集合为 \(P\):
-
若 \(P\) 为空集,说明 \(v\) 插入后不变,显然成立
-
反之,考虑需异或出来的数 \(v \oplus \{x_i\} \oplus \{y_j\}\),其中 \(\{x_i\} \subseteq P, \{y_j\} \subseteq (S-P)\)
由于 \(\{y_j\}\) 中的元素不变,原来能选现在也能选
插入 \(v\) 后,相当于令 \(v' \leftarrow v \oplus P\);则有 \(v \oplus \{x_i\} = v' \oplus (P-\{x_i\})\)
综上,仍然能凑出来,证毕
性质 4:\(|S|\) 唯一且最小
证明:归纳,设原线性基为 \(S\),大小为 \(\text{siz}\),插入 \(v\) 前合法:
-
若 \(v\) 未成功插入,线性基大小不变,仍然唯一且最小
-
若 \(v\) 成功插入,线性基大小 \(+1\);由性质 2 + 性质 3,此时 \(|T_{\{a_i\} \cup \{v\}}|\) 为 \(2^{\text{siz}+1}\)
反证,若线性基大小不变,由性质 2,最多异或出 \(2^{\text{siz}}\) 个数,不合法;综上,证毕
-
求 \(\max T_{\{a_i\}}\):由性质 3,只需考虑在线性基集合 \(S\) 中选若干个数异或得到的 \(\max\)
考虑从高位到低位贪心,设当前答案为 \(\text{ans}\),每次令 \(\text{ans} \leftarrow \max(\text{ans}, \text{ans} \oplus p_i)\)
由于 \(p_i\) 的二进制最高位为 \(i\),相当于每次贪心地选令 \(\text{ans}\) 第 \(i\) 位为 \(1\) 的方案
求 \(\min T_{\{a_i\}}\):\(\min p_i\) 即为答案 (异或其他任意 \(p_j\) 都会变大),特判能否异或出 \(0\)
求 \(\text{kthmin}\ T_{\{a_i\}}\):若异或 \(p_i\) 后 \(\text{ans}\) 第 \(i\) 位为 \(1\),则不异或 \(p_i\),任意异或 \(p_{0 \sim i-1}\) 都不优
因此,此时至少有 \(2^{i-1}-1\) 个数比 \(\text{ans}\) 小,若 \(k \ge 2^{i-1}\),则必定选择异或 \(p_i\)
考虑将 \(k\) 二进制分解,若第 \(i\) 位为 \(1\),则令 \(\text{ans} \leftarrow \text{ans} \oplus p_i\)
问题在于,\(p_i\) 的第 \(0 \sim i-1\) 位可能存在 \(1\),异或 \(p_i\) 后再异或 \(p_{0 \sim i-1}\) 可能会消去
因此,考虑对线性基进行重构,若 \(p_i\) 的第 \(j\ (j < i)\) 位为 \(1\) 则令 \(p_i \leftarrow p_i \oplus p_j\)
单次重构复杂度 \(O(\log^2 V)\),查询复杂度 \(O(\log V)\)
**P6109 [Ynoi2009] rprmq1 (对修改差分 + 猫树分治 + 区间加区间历史 \(\max\) )
有 \(n \times n\) 的矩阵 \(a_{i, j}\),初始全为 \(0\),有 \(m\) 次修改和 \(q\) 次查询,先进行所有修改,再进行所有查询
一次修改会给出 \(l_1, l_2, r_1, r_2, x\),表示把 \(l_1 \le i \le r_1, l_2 \le j \le r_2\) 的 \(a_{i, j}\) 加上 \(x\)
一次查询会给出 \(l_1, l_2, r_1, r_2, x\),表示查询 \(l_1 \le i \le r_1, l_2 \le j \le r_2\) 的 \(a_{i, j}\) 的 \(\max\)
\(1 \le n, m \le 5 \cdot 10^4\),\(1 \le q \le 5 \cdot 10^5\),\(1 \le x \le 2^{31}-1\);4s, 512MB
考虑将修改差分,即在 \(l_1\) 做 \([l_2, r_2] + x\),在 \(r_1+1\) 做 \([l_2, r_2]-x\)
对询问 \((l_1, r_1, l_2, r_2)\),加入前缀 \([1, l_1-1]\) 的修改,对 \([l_1, r_1]\) 的修改维护区间历史 \(\max\),在 \(r_2\) 查 \([l_2, r_2]\) 即可
不能直接扫描线的问题是,我们不好撤销对区间历史 \(\max\) 的贡献
考虑猫树分治,核心思想是固定 \(\text{mid}\) / \(\text{mid}+1\) 为区间一端,撤销时直接改为当前区间 \(\max\):
-
拎出跨过 \(\text{mid}\) 的询问区间 \([l', r']\),首先考虑 \([\text{mid}+1, r']\) 的贡献:
-
首先,加入 \([l, \text{mid}]\) 的修改 ( \([1, l-1]\) 的修改之前加入,类似线段树分治从左到右做)
-
从 \(\text{mid}+1\) 向 \(r\) 扫;对每个位置 \(i\),先加入 \(i\) 处的修改,维护区间历史 \(\max\),再处理 \(r' = i\) 的询问
当 \(i = \text{mid}+1\) 时,在加入修改后,处理询问前,令所有区间的历史 \(\max\) 都置为当前区间 \(\max\)
这么做的道理是,我们不应统计 \([1, \text{mid}]\) 的修改的历史 \(\max\),相当于强制从 \(\text{mid}+1\) 开始统计
-
-
统计完后,将 \([\text{mid}+1, r]\) 的修改撤销 (直接减),递归求解 \([\text{mid}+1, r]\)
此时,\([l, \text{mid}]\) 的修改仍然在,正确性每问题
由于我们 "区间历史 \(\max\) 置为当前区间 \(\max\) " 的策略,此处不需考虑对历史 \(\max\) 的影响
-
再考虑 \([l', \text{mid}]\) 的贡献:
-
此时,\([\text{mid}+1, r]\) 的修改已经撤销,无需考虑
-
从 \(\text{mid}\) 向 \(l\) 扫;对每个位置 \(i\),先处理 \(l' = i\) 的询问,再撤销 \(i\) 处的修改 (维护区间历史 \(\max\) )
类似的,当 \(i = \text{mid}\),在处理询问前,令所有的区间历史 \(\max\) 都置为当前区间 \(\max\)
-
-
最终,\([l, \text{mid}]\) 的所有修改也会被撤销,递归求解 \([l, \text{mid}]\) 即可
时间复杂度 \(O((n+q) \log^2 n)\)
实现细节:
-
对同一位置的修改,按权从小到大排序,避免有加有减时误把前面的若干加算进历史 \(\max\)
撤销时,应按权从大到小撤销
历史 \(\max\) / 历史和
P4314 CPU 监控 (线段树 - 区间历史 \(\max\),矩乘维护)
给定长为 \(n\) 的序列 \(a_i\),有 \(m\) 次操作,每次为以下 \(4\) 种之一:
- 给定 \(l_i, r_i\),询问 \([l_i, r_i]\) 的 \(\max\)
- 给定 \(l_i, r_i\),询问 \([l_i, r_i]\) 的历史 \(\max\)
- 给定 \(l_i, r_i, w_i\),对 \([l_i, r_i]\) 区间加 \(w_i\)
- 给定 \(l_i, r_i, w_i\),将 \([l_i, r_i]\) 推平为 \(w_i\)
\(1 \le n, m \le 10^5\),\(-2^{31} \le V \le 2^{31}\)
考虑矩乘维护,这样不需要推导懒标记间的关系
传统矩乘为 \(C_{x, y} = \sum A_{x, i} \cdot B_{i, y}\),此处使用 \((\max, +)\) 的广义矩乘,即 \(C_{x, y} = \max(A_{x, i} + B_{i, y})\)
- 传统矩乘与广义矩乘都满足结合律
对线段树上区间 \([l, r]\),维护向量 \(\begin{bmatrix} a_i \\ b_i \\ 0 \end{bmatrix}\),\(a_i\) 表示区间 \(\max\),\(b_i\) 表示区间历史 \(\max\),\(0\) 用来辅助推平操作
对区间加 \(k\),转移形如 \(\begin{bmatrix} k & -\infty & -\infty \\ k & 0 & -\infty \\ -\infty & -\infty & 0 \end{bmatrix} \begin{bmatrix} a_i \\ b_i \\ 0 \end{bmatrix} = \begin{bmatrix} a_i+k \\ \max(b_i, a_i+k) \\ 0 \end{bmatrix}\)
对区间推平为 \(k\),转移形如 \(\begin{bmatrix} -\infty & -\infty & k \\ -\infty & 0 & k \\ -\infty & -\infty & 0 \end{bmatrix} \begin{bmatrix} a_i \\ b_i \\ 0 \end{bmatrix} = \begin{bmatrix} k \\ \max(b_i, k) \\ 0 \end{bmatrix}\)
综上,只需记录区间乘矩阵的 tag,正常 pushdown 即可;pushup 时对儿子向量对位取 \(\max\)
实现细节:
-
可以封装向量 + 矩阵,重载
+和*运算符 -
除却 \(-\infty\) 和 \(0\) 这些常量,矩阵中实际只有 \(x_{1, 1}, x_{1, 3}, x_{2, 1}, x_{2, 3}\) 是变量,可以只维护这些位置
同理,对向量也可以只维护 \(x_{1, 1}, x_{2, 1}\)
-
注意矩乘没有交换律,每次都是左乘
时间复杂度 \(O(n \log n)\)
*P8868 [NOIP2022] 比赛 (扫描线 + 线段树历史和 - 矩乘 + 区间覆盖转区间加 - 差分消去多余贡献)
给定两长为 \(n\) 的序列 \(a_i\) 与 \(b_i\)
\(q\) 次询问,每次给定 \(l_i, r_i\),求 \(\sum\limits_{l_i \le p \le q \le r_i} \max\limits_{p \le k \le q} a_k \cdot \max\limits_{p \le k \le q} b_k\);答案对 \(2^{64}\) 取模
\(1 \le n, q \le 2.5 \cdot 10^5\),保证 \(\{a_i\}, \{b_i\}\) 分别构成 \(1\) 到 \(n\) 的排列;2s, 512MB
对 \(r\) 扫描线,动态维护 \(a'_i = \max\limits_{i \le k \le r} a_k\) 与 \(b_i' = \max\limits_{i \le k \le r} b_k\)
令 \(c_i' = a_i'b_i'\),则 \(i\) 位置的 \(c_i'\) 历史和代表 \([i, i \sim r]\) 的贡献和;对询问 \([L, R]\),在 \(r = R\) 时询问 \([L, R]\) 区间历史和即为答案
直接做为以 \(r\) 结尾的后缀推平 + 区间历史和
这么做有点难受,因为只有 \(a_i', b_i'\) 全修改完后才能对历史和贡献
考虑转成区间加,差分消去多余贡献,在单调栈上做:
-
设栈顶为 \(r'\),栈顶下一个数为 \(l'\)
若 \(a_i < a_{r'}\),无事发生,只需加入 \([i, i]\) 的贡献
反之,\((l', r']\) 本来由 \(a_{r'}\) 管辖,现在应该由 \(a_i\) 管辖,将 \((l', r']\) 区间加 \(a_i-a_{r'}\),累加当前贡献 + 消去之前贡献
将 \(r'\) 弹出,继续考虑下一个数;容易发现这样不重不漏
-
当扫到 \(r\) 时,令 \([1, r]\) 整体对历史和贡献 \(1\) 次 (注意不是 \([1, n]\),这样会对后面算重)
考虑矩乘维护 \(a_i', b_i'\) 区间加 + \(a_i'b_i'\) 历史和
维护 \(\begin{bmatrix} a''_i \\ b''_i \\ s_i \\ h_i \\ \text{len} \end{bmatrix}\),表示 \(a_i'\) 区间和,\(b_i'\) 区间和,\(a_i'b_i'\) 区间和,\(a_i'b_i'\) 区间历史和,区间长度
前两个用于维护 \(s_i, h_i\),\(\text{len}\) 用于维护 \(a_i'', b_i''\)
这里直接用经典矩乘就是最简单的
对 \(a_i'\) 区间加 \(k\),转移形如 \(\begin{bmatrix} 1 & 0 & 0 & 0 & k \\ 0 & 1 & 0 & 0 &0 \\ 0 & k & 1 & 0 & 0 \\ 0 & 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} a_i'' \\ b_i'' \\ s_i \\ h_i \\ \text{len} \end{bmatrix} = \begin{bmatrix} a_i'' + k \cdot \text{len} \\ b_i'' \\ s_i + k \cdot b_i'' \\ h_i \\ \text{len} \end{bmatrix}\)
对 \(b_i'\) 区间加 \(k\),转移形如 \(\begin{bmatrix} 1 & 0 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 & k \\ k & 0 & 1 & 0 & 0 \\ 0 & 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} a_i'' \\ b_i'' \\ s_i \\ h_i \\ \text{len} \end{bmatrix} = \begin{bmatrix} a_i'' \\ b_i'' + k \cdot \text{len} \\ s_i + k \cdot a_i'' \\ h_i \\ \text{len} \end{bmatrix}\)
整体对历史和贡献,转移形如 \(\begin{bmatrix} 1 & 0 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 & 0 \\ 0 & 0 & 1 & 0 & 0 \\ 0 & 0 & 1 & 1 & 0 \\ 0 & 0 & 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} a_i'' \\ b_i'' \\ s_i \\ h_i \\ \text{len} \end{bmatrix} = \begin{bmatrix} a_i'' \\ b_i'' \\ s_i \\ h_i + s_i \\ \text{len} \end{bmatrix}\)
手模下,矩阵只有 \(x_{1, 5}, x_{2, 5}, x_{3, 1}, x_{3, 2}, x_{3, 5}, x_{4, 1}, x_{4, 2}, x_{4, 3}, x_{4, 5}\) 这 \(9\) 个位置是变量,只需维护这些位置即可
时间复杂度 \(O((n+q) \log n)\)
二进制分组
CF710F - String Set Queries (二进制分组 + AC 自动机)
你需要维护字符串集合 \(D\),处理 \(m\) 个查询,每个查询为以下三种类型之一:
- 向集合 \(D\) 中添加 \(1\) 个字符串 \(s\),保证 \(s\) 之前不在 \(D\) 中
- 从集合 \(D\) 中删除 \(1\) 个字符串 \(s\),保证 \(s\) 之前在 \(D\) 中
- 给定字符串 \(s\),求 \(D\) 中所有字符串在 \(s\) 中出现的次数总和;若同一个串出现多次,应重复计数
\(1 \le m \le 3 \cdot 10^5\),保证所有字符串总长度 \(\le 3 \cdot 10^5\),强制在线;3s, 750MB
考虑所有修改都在询问前面怎么做
对 \(D\) 建 AC 自动机,添加看成 \(+1\),删除看成 \(-1\),预处理 fail 树上到根链权和,查询时直接在 AC 自动机上走即可
对于强制在线,考虑如下做法:
-
注意到贡献可拆分,若有多个 AC 自动机,将询问串在每个自动机上走一遍,累加答案即可
-
每次修改都重构,复杂度无法接受,考虑只在特定时刻重构
具体的,维护自动机序列,记录每个自动机的大小 \(\text{siz}_i\),设当前有 \(\text{tot}\) 个自动机
修改时,先将 \(s\) 插入第 \(\text{tot}\) 个自动机:
-
若 \(\text{siz}_{\text{tot}} \ne \text{siz}_{\text{tot-1}}\),正常插入
-
反之,插入后将第 \(\text{tot}\) 与第 \(\text{tot}-1\) 个自动机合并,若第 \(\text{tot}-1\) 与第 \(\text{tot}-2\) 个自动机大小相同,则再次合并
综上,一直合并到不满足合并条件时停止
最终,对合并后第 \(\text{tot}\) 个自动机暴力重构 fail 树
-
-
分析下复杂度:
容易发现,同一时刻最多存在 \(O(\log m)\) 个自动机,查询复杂度 \(O(\sum |s_i| \log m)\)
对修改,若两自动机合并,大小必定 \(\times 2\),因此最多重构 \(O(\log m)\) 次,修改复杂度 \(O(\sum |s_i| \log m)\)
综上,总复杂度 \(O(\sum |s_i| \log m)\)
事实上,\(t\) 次修改后存在的自动机,可以映射到 \(t\) 二进制拆分后的每个 \(1\),大小即对应的 \(2^k\)
以下是 AC 自动机复习部分:
-
AC 自动机,是以 trie 的结构为基础,结合 KMP 的思想建立的自动机
设状态集合为 \(Q\);对每个 \(u \in Q\),维护失配指针 \(\text{fail}\) 指向 \(v\),满足 \(v \in Q\) 且 \(v\) 是 \(u\) 的最长后缀
容易发现,所有 fail 边构成一棵树 (只向深度小的连边,最终都汇聚到根)
本质上,AC 自动机希望通过跳 fail 树维护当前前缀的所有后缀
-
考虑如何构建 fail 指针:
-
按深度从小到大 bfs,增量构造,设当前节点为 \(u\),trie 上父亲为 \(\text{fa}_u\),转移边为 \((\text{fa}_u, u, c)\)
-
若 \(\text{trie}(\text{fail}(\text{fa}_u), c)\) 存在,令 \(\text{fail}(u) \leftarrow \text{trie}(\text{fail}(\text{fa}_u), c)\),即在父亲的最长后缀后面拼上 \(c\)
反之,跳到 \(\text{fail}(\text{fail}(\text{fa}_u))\),一直跳直到存在 \(c\) 的转移边,更新 \(\text{fail}(u)\)
若依然不存在,令 \(\text{fail}(u)\) 指向根
-
实现上,为避免重复跳 fail,考虑在 trie 中引入不存在的状态,压缩路径
具体的,若 \((u, v, c)\) 不存在,记存在 \((\text{fail}(v), v', c)\),创建转移边 \((u, v', c)\)
本质上,若儿子存在,则跳到儿子就会结束;反之,从后面更新跳到的位置
构建 fail 时,记存在 \((\text{fail}(\text{fa}_u), v, c)\),直接令 \(\text{fail}(u) = v\) 即可
-
-
多模式匹配:
- 预处理 fail 树上每个点到根链权和,查询时,直接在 AC 自动机上走,累加权和即可
主席树 + 线段树分裂合并
P5494 【模板】线段树分裂 (线段树分裂合并)
给出可重集 \(a\) (编号为 \(1\) ),有 \(m\) 次操作,每次形如以下五种:
0 p x y:将 \(p\) 中在 \([x, y]\) 内的值移动到新的可重集中 (上次产生的新可重集的编号 \(+1\) )1 p t:将 \(t\) 中所有值移动到 \(p\),清空 \(t\) (保证以后不会用到 \(t\) )2 p x q:在 \(p\) 中加入 \(x\) 个数字 \(q\)3 p x y:查询 \(p\) 中在 \([x, y]\) 内的值的个数4 p k:查询 \(p\) 中第 \(k\) 小的数,不存在输出-1\(1 \le n \le 2 \cdot 10^5\),\(1 \le x, y, q \le m \le 2 \cdot 10^5\);1s, 128MB
[!NOTE]
划分可重集值域 + 可重集合并 \(\to\) 线段树分裂 + 线段树合并
考虑 FHQ treap 按排名分裂的思路:
-
设当前
split(x, &y, k),表示将排名 \([1, k]\) 的值分裂到 \(x\) 中,其余分裂到 \(y\) 中 -
设 \(x\) 左子树中值的个数为 \(v\):
- 若 \(v < k\),左子树应分裂到 \(x\) 中,递归分裂
split(x.rs, y.rs, k-v) - 若 \(v = k\),右子树应分裂到 \(y\) 中,将 \(x\) 的右儿子置 \(0\) 即可
- 若 \(v > k\),右子树应分裂到 \(y\) 中,将 \(x\) 的右儿子置 \(0\),递归分裂
split(x.ls, y.ls, k)
- 若 \(v < k\),左子树应分裂到 \(x\) 中,递归分裂
对操作 \(0\),先查 \(x, y\) 的排名,分裂出 \([1, x-1] + [x, n]\),再分裂 \([x, y] + [y+1, n]\)
操作 \(1\) 线段树合并,操作 \(2\) 单点改,操作 \(3\) 区间查,操作 \(4\) 线段树上二分即可
分析下复杂度:
-
若没有分裂,最多加 \(m\) 个叶子,到根链交集和是 \(O(m \log n)\) 的
-
对分裂,每次分裂只会单侧递归,单次复杂度 \(O(\log n)\)
对合并,注意到分裂后至多 \(1\) 个树有 \(2\) 个儿子,两树有先后关系,同样只会单侧递归,单次复杂度 \(O(\log n)\)
综上,时间复杂度 \(O(m \log n)\)
*P5298 [PKUWC2018] Minimax (线段树合并优化 DP)
给定 \(n\) 个点的有根树,以 \(1\) 为根,每个点最多有两个儿子
定义节点 \(u\) 的权值为:
- 若 \(u\) 没有儿子,其输入权值 \(p_u\) 即为真实权值,保证这类点的权值互不相同
- 若 \(u\) 有儿子,其权值有 \(p_u\) 的概率是其儿子权值的 \(\max\),有 \(1-p_u\) 的概率是其儿子权值的 \(\min\)
假设 \(1\) 号点的权值有 \(m\) 种可能,记第 \(i\) 小可能权值为 \(v_i\),其概率为 \(d_i\),求 \(\sum\limits_{i=1}^{m} i \cdot v_i \cdot d_i^2\)
答案对 \(998244353\) 取模
\(1 \le n \le 3 \cdot 10^5\),\(1 \le p_i \le 10^9\),若 \(p_i\) 表示概率,则输入值为 \(p_i \cdot 10000\);1s, 500MB
[!NOTE]
\(O(n^2)\) 二维 DP \(\to\) 快速维护前缀和 + 后缀和 + 对位乘 \(\to\) 线段树合并优化 DP
法 1:
枚举 \(1\) 号点最终权 \(w\),考虑 median heap 的 trick,按与 \(w\) 的大小关系分成 \(0/1\),DP \(f_{u, 0/1}\),差分得到真实概率
直接做 \(O(n^2)\),注意到 \(1\) 次只会改 \(O(1)\) 个点权,据说可以动态 DP 优化
法 2:
每次维护 \(0/1\) 不好做,考虑直接把状态变成二维的
记 \(f_{u, i}\) 表示点 \(u\) 权的全局排名为 \(i\) 的概率
转移时,若只有 \(1\) 个儿子则直接继承,若有 \(2\) 个儿子 \(\text{ls}, \text{rs}\),则形如 \(f_{u, i} \leftarrow f_{\text{ls}, i} \cdot (p_u \cdot \sum\limits_{j \le i} f_{\text{rs}, j} + (1-p_u) \cdot \sum\limits_{j \ge i} f_{\text{rs}, j})\)
需要快速维护前缀和 + 后缀和 + 对位乘,考虑线段树合并优化 DP:
-
在 merge 的过程中多记前缀和 + 后缀和,改为
merge(u, v, w1, w2),这里w1, w2各自维护 \(f_{u, i}, f_{v, i}\) 的转移系数对
w1, w2的维护,往左子树走时拼上右子树的和,往右子树走时拼上左子树的和即可 -
当 \(u, v\) 有 \(1\) 个不存在时,容易发现转移系数已不会改变 (其中 \(1\) 个子树是空的,增量始终为 \(0\) )
因此,直接在非空子树打上区间乘标记即可
-
对标记下传,递归合并左右儿子前
pushdown,合并完成后pushup即可
离散化,时间复杂度 \(O(n \log n)\)
P3224 [HNOI2012] 永无乡 (线段树合并维护连通块信息)
*P2839 [国家集训队] middle (二分转判定 - 转正负 1 + 分析变化量 - 主席树)
对长度为 \(n\) 的序列 \(a_i\),将其从小到大排序得到 \(b_i\),其中位数定义为 \(b_{\lfloor \frac{n}{2} \rfloor}\) (0-index)
给定长为 \(n\) 的序列 \(s_i\),记 \(f(l, r)\) 表示 \(s_{[l, r]}\) 的中位数
有 \(q\) 次询问,第 \(i\) 次给定 \(a < b < c < d\),求 \(\max\limits_{l \in [a, b], r \in [c, d]} f(l, r)\)
强制在线;\(1 \le n \le 2 \cdot 10^4\),\(1 \le q \le 2.5 \cdot 10^4\),\(1 \le a_i \le 10^9\);2s, 512MB
[!NOTE]
中位数 \(\to\) 转 \(\pm 1\) 权值判定问题 \(\to\) 维护最大前后缀 \(\to\) 变化量为 \(1\),主席树
[!IMPORTANT]
Trick:中位数判定转 \(\pm 1\)
考虑对于值 \(w\) 与区间 \([l, r]\),如何判定 \(w\) 是 \(s_{[l, r]}\) 的中位数
将 \(< w\) 的置为 \(-1\),\(\ge w\) 的置为 \(+1\),等价于 \([l, r]\) 区间和 \(\ge 0\)
对询问 \((a, b, c, d)\),求出 \([a, b]\) 的最大后缀和 + \([b+1, c-1]\) 的区间和 + \([c, d]\) 的最大前缀和,check 即可
转二分,显然 \(w\) 越大对前后缀和的限制越严,有单调性
对每个 \(w\) 预处理,注意到 \(w \to w+1\) 时只有 \(s_i = w\) 的位置从 \(1\) 变成 \(-1\),主席树维护即可
时间复杂度 \(O(n \log n + q \log n)\)
P6071 『MdOI R1』Treequery (分讨刻画路径交 + st 表区间 lca + 主席树维护前驱后继 / 区间 max)
P3899 [湖南集训] 更为厉害 (主席树维护二维数点)
P4197 [ONTAK2010] Peaks (按边权从小到大做 + 线段树合并维护连通块 / Kruskal 重构树 + 倍增 + 主席树)
给定 \(n\) 个点 \(m\) 条边的无向图,点有点权,边有边权
\(q\) 次询问,第 \(i\) 次给定 \(v, x, k\),查询从 \(v\) 开始只走边权 \(\le x\) 路径,所有能到达的点中的第 \(k\) 小点权
[!NOTE]
法 1:只走边权 \(\le w\) \(\to\) 离线从小到大加,第 \(k\) 小 \(\to\) 线段树合并维护连通块
法 2:只走边权 \(\le w\) \(\to\) Kruskal 重构树,第 \(k\) 小 \(\to\) 树上主席树
[!IMPORTANT]
Trick:Kruskal 重构树维护只走边权 \(\le w\) 的边能到达的点
法 1:离线,边权从小到大做,并查集 + 线段树合并维护连通块
法 2:考虑 Kruskal 重构树,在合并 \(u, v\) 时建新点,点权为当前边权,左右儿子为 \(u, v\) 的代表元
性质 1:\(u, v\) 间所有简单路径最大边权最小值 = \(u, v\) 的 \(\text{lca}\) 的点权
性质 2:从 \(u\) 开始,只走 \(\le w\) 的边权,能到达的所有点 = 找到 \(u\) 到根链上最浅的点权 \(\le w\) 的点,其子树内的点
倍增 + 树上主席树维护即可
*P3293 [SCOI2016] 美味 (主席树维护值域平移区间异或 \(\max\) )
给定长为 \(n\) 的序列 \(a_i\),有 \(m\) 次询问,第 \(i\) 次给定 \(b_i, x_i, l_i, r_i\),询问 \(\max_{l_i \le j \le r_i} b_i \oplus (a_j+x_i)\)
\(1 \le n \le 2 \cdot 10^5\),\(1 \le m \le 10^5\),\(0 \le a_i, b_i, x_i < 10^5\);3s, 252MB
[!NOTE]
定值对区间异或 \(\max\) \(\to\) 按位贪心 \(\to\) 主席树维护出现次数 check
定值对区间异或 \(\max\),容易想到 01 trie,可惜它不支持值域平移
考虑回归本源,按位贪心,低位构成区间,减去 \(x_i\) 后主席树维护出现次数 check 即可
时间复杂度 \(O(n \log V + m \log^2 V)\)
splay + LCT
Splay
有旋平衡树,靠旋转保证树平衡
在每个节点 \(u\),维护:
-
\(\text{fa}_u\) 表示父亲,\({\text{ch}_{0, 1}}_u\) 表示左右儿子 (树结构)
-
\(w_u\) 表示值,\(c_u\) 表示 \(w_u\) 的出现次数 (节点信息)
-
\(\text{siz}_u\) 表示子树大小 (树信息)
-
旋转操作
void rot(int u):-
目标:将 \(u\) 上旋 \(1\) 层,维持树的中序遍历不变
\(u\) 为左儿子时称为 "右旋",为右儿子时称为 "左旋"
-
例:\(\text{fa} = \{0, 1, 1, 2, 2\}\),对节点 \(2\) 右旋后变为 \(\text{fa} = \{2, 0, 1, 2, 1\}\),中序遍历始终为 \(4, 2, 5, 1, 3\)
-
以右旋为例,记 \(\text{fa}_1 = \text{fa}_u\),\(\text{fa}_2 = \text{fa}_{\text{fa}_1}\),分析旋转操作带来的影响:
- 对 \(\text{fa}_1\),旋转后将变为 \(u\) 的右儿子;为保证中序遍历不变,原来 \(u\) 的右儿子将成为 \(\text{fa}_1\) 的左儿子
- 对 \(u\),其左儿子不变,右儿子将变为 \(\text{fa}_1\),父亲将变为 \(\text{fa}_2\)
- 综上,修改 \({\text{ch}_1}_u, u, \text{fa}_1, \text{fa}_2\) 间的父子关系即可
-
注意,修改树结构后需要
pushup更新 \(\text{siz}_u, \text{siz}_{\text{fa}_1}\)
-
-
伸展操作
void splay(int& rt, int u):-
目标:将 \(u\) 上旋至 \(\text{rt}\),成为原 \(\text{rt}\) 子树的根
-
有 \(3\) 种上旋形式:
-
当 \(\text{fa}_u =\text{rt}\),直接对 \(u\) 上旋 \(1\) 次
-
当 \(u, \text{fa}_u\) 同为各自父亲的左 / 右儿子,先上旋 \(\text{fa}_u\),再上旋 \(u\)
手模下容易发现,这样比上旋两次 \(u\) 更平衡
-
反之,将 \(u\) 上旋两次
-
-
对每轮,判断使用哪种上旋形式,把 \(u\) 旋上来即可
实现上,可以记录 \(\text{fa}_\text{rt}\),当 \(\text{fa}_u = \text{fa}_\text{rt}\) 时说明旋到根了
-
容易发现,伸展操作会将 \(u\) 到根链上的所有节点信息都更新一遍
-
-
平衡树操作:
-
按值查找
void find(int& rt, int w):- 目标:将值 \(w\) 对应节点旋到根,若不存在则将找 \(w\) 时最后经过的点旋上来
- 按 BST 的性质向左右子树找即可,最后需要
splay一下 - 性质:若最终 \(w_{\text{rt}} \ne w\),其必为 \(w\) 的前驱 / 后继
-
按排名查找
void loc(int& rt, int rk):- 目标:将排名为 \(\text{rk}\) 的节点旋到根 (要求 \(\text{rk} \le \text{siz}_{\text{rt}}\) )
- 按 BST 的性质,判断左子树 \(\text{siz}\) / 左子树 \(\text{siz}\) + 当前点 \(c\) 与 \(\text{rk}\) 的大小关系,向左右子树找即可,最后需要
splay一下
-
合并
int merge(int u, int v):- 目标:合并以 \(u\) 为根 + 以 \(v\) 为根的两棵 splay,要求 \(u\) 内最大值严格小于 \(v\) 内最小值,返回合并后根节点
- 一侧为空返回另一侧,反之
loc(v, 1)后把 \(u\) 挂在 \(v\) 的左子树即可,记得pushup
-
插入
void ins(int w):- 类似按值查找,先尝试找到 \(w\):
- 若找到,将对应 \(\text{siz}, c\) 加 \(1\) 即可
- 反之,创建新节点,将其挂在找 \(w\) 时最后经过的节点上
- 最后需要
splay一下
- 类似按值查找,先尝试找到 \(w\):
-
删除
void del(int w):直接find,将根节点对应 \(\text{siz}, c\) 减 \(1\),若 \(c\) 减为 \(0\) 则merge左右儿子形成新 splay,记得左右儿子 \(\text{fa}\) 置 \(0\) -
按排名查值
int qval(int rk):特判不存在,反之直接loc,返回 \(v_{\text{rt}}\) 即可 -
按值查排名
int qrk(int w):直接find,由于 \(w\) 可能不存在,特判 \(v_{\text{rt}}\) 与 \(w\) 的大小关系来加 \(c_{\text{rt}}\) -
查前驱
int qprev(int w):直接find,若 \(v_{\text{rt}} > w\) 就再在左子树里查 (一直走右儿子即可),最后需要splay一下 -
查后继
int qnext(int w):直接find,若 \(v_{\text{rt}} < w\) 就再在右子树里查 (一直走左儿子即可),最后需要splay一下
-
-
区间操作:
-
建树时,由 BST 性质,新建节点后将 \(\text{rt}\) 置为左儿子 (维护左链),最后
splay一下 -
对 \([l, r]\) 操作时,先将排名为 \(l-1\) 的旋到根,再将右子树中排名为 \(r-l+2\) 的旋到根
此时,右子树的左子树即为 \([l, r]\),更新信息 + 下传标记 +
splay一下即可 (这里splay是为了维护树信息)注意,在
loc中,也要在访问新节点前先下传标记
-
单次均摊复杂度 \(O(\log n)\)
P3690 【模板】动态树(LCT)
给定 \(n\) 个点,点有点权,有 \(m\) 次操作,每次形如以下四种:
0 x y,表示询问 \(x\) 到 \(y\) 路径上所有点的点权异或和1 x y,表示连边 \((x, y)\),若 \(x, y\) 已经连通则无需连边2 x y,表示删边 \((x, y)\),若 \((x, y)\) 不存在则无事发生3 x y,表示令 \(x\) 点权变为 \(y\)\(1 \le n \le 10^5\),\(1 \le m \le 3 \cdot 10^5\),\(1 \le a_i \le 10^9\);1s, 128MB
思想:对整棵树进行剖分,每条链用数据结构维护
为保证灵活性,采用实链剖分:
-
每个点 \(u\) 有至多 \(1\) 个实儿子 \(\text{son}_u\)
-
将 \((u, \text{son}_u)\) 称为实边,其余边称为虚边;由实边构成的链称为实链
对每条实链,用 splay 维护链上信息
为支持加边 / 断边,构建辅助树:
-
原树节点与辅助树节点一一对应
-
辅助树由多棵 splay 组成,每个 splay 维护原树的 \(1\) 条实链
中序遍历 splay,可得原树上按 \(\text{dep}\) 从小到大 (即 "从上向下" ) 遍历实链的点序列
-
splay 间不独立,每棵 splay 的根节点 (注意不是原树实链顶) 的 \(\text{fa}\) 指向原树实链顶的父亲
与 splay 中父子关系不同的是,这里儿子认父亲,父亲不认儿子
LCT 的操作:
-
数据结构函数:
-
bool dir(int u):判断 \(u\) 是父亲的左儿子 / 右儿子 -
bool isRoot(int u):判断 \(u\) 是否是所在 splay 的根,实现上,直接判父亲的两个儿子都不是 \(u\) 即可 -
void pushup(int u):更新 \(u\) 子树信息 -
void maintain(int u):下传 \(u\) 的标记 (区间翻转等) -
void pushdown(int u):将 \(u\) 在 splay 中到根链上的点全部maintain一遍 -
void rot(int u):将 \(u\) 上旋 \(1\) 层;注意,这里判断 \(\text{fa}_2\) 是否存在时应该用isRoot -
void splay(int u):将 \(u\) 上旋到所在 splay 的根;记得上旋前先pushdown,判断根时也应该用isRoot
-
-
LCT 函数:
-
void access(int u):将 \(u\) 到原树根置为实链辅助树上,首先将 \(u\)
splay到根,此时 \(u\) 左子树中的点在原树上都在 \(u\) 到根链上,右子树中的点不在,应该扔掉因此,将 \(u\) 到右儿子的边断掉 (单方面改儿子,不动儿子的 \(\text{fa}\),因为在原树上是虚边)
此时,\(u\) 与 \(\text{fa}_u\) 所在实链应当合并,将 \(\text{fa}_u\) 旋到根后,把 \(\text{fa}_u\) 的右儿子置为 \(u\) 即可
重复上述过程,当前点旋到根 + 右儿子改为上 \(1\) 层的根 +
pushup即可 -
void makeRoot(int u):将原树的根换为 \(u\)首先,执行
access(u), splay(u)容易发现,
makeRoot(u)只会对 \(u\) 到根链产生影响 (先后关系反转),对其他子树没有影响因此,将 \(u\) 到根链对应的 splay 打上区间翻转 tag 即可 (即在 \(u\) 处打,因为
splay后 \(u\) 就是该 splay 的根) -
int find(int u):找到 \(u\) 在原树上的根首先,执行
access(u), splay(u), maintain(u)一直走 \(u\) 的左儿子 (即在原树上跳到根链),记录最后到的节点 \(u'\),最后把 \(u'\)
splay上来,返回 \(u'\) 即可 -
void link(int u, int v):若 \(u, v\) 不在同个连通块中,则连边 \((u, v)\)判断 \(u, v\) 是否在同个连通块,可以
find(u) == find(v)执行
makeRoot(u), splay(u)后把 \(u\) 挂到 \(v\) 上即可 (单方面认父,相当于连虚边) -
void cut(int u, int v):若 \((u, v)\) 边存在,则删去 \((u, v)\)首先,执行
makeRoot(u), access(v), splay(v)此时,原树上 \((u, v)\) 存在等价于:
- splay 上 \(\text{fa}_u = v\) 且 \(u\) 没有右儿子 ( \(v\) 是 splay 的根,遍历原树实链时 \(u, v\) 间无其他点 )
- \(u, v\) 在同个连通块
注意,
find会改变 splay 结构,需要在find后再次makeRoot(u), access(v), splay(v)显然 \(u\) 是 \(v\) 的左儿子,将 \((u, v)\) 双向断开 ( \(\text{fa}_u\) 置 \(0\),\(v\) 左儿子置 \(0\) ) 即可,记得要
pushup
-
-
本题操作:
- 单点改,将对应点
splay到根后直接改点权即可 - 路径查,
makeRoot(u), access(v), splay(v)后直接查 \(v\) 的子树异或和即可
- 单点改,将对应点
单次均摊复杂度 \(O(\log n)\)
*P4219 [BJOI2014] 大融合 (LCT 维护子树信息 / 线段树分治)
给定 \(n\) 个点,有 \(q\) 次操作,每次形如以下两种:
A x y,表示连边 \((x, y)\),保证 \((x, y)\) 之前不存在Q x y,保证 \((x, y)\) 存在,求有多少条简单路径包含 \((x, y)\)\(1 \le n, q \le 10^5\);1s, 125MB
[!NOTE]
法 1:对询问,考虑删去 \((x, y)\),将 \(x, y\) 连通块大小乘起来 \(\to\) 离线,线段树分治
法 2:对询问,考虑删去 \((x, y)\),将 \(x, y\) 连通块大小乘起来 \(\to\) 在线,LCT 维护子树信息
[!IMPORTANT]
Trick:LCT 维护子树信息
转化为加边 + 删边 + 查询连通块大小
法 1:离线,线段树分治 + 可撤销并查集
法 2:LCT 维护子树信息
不能直接做的原因是,每个点还连着若干虚子树,由于认父不认子,不能直接统计这些虚子树的贡献
考虑对每个点 \(u\),单独记录虚子树的信息
记 \(\text{siz}_u\) 表示 \(u\) 在 splay 上的子树大小 (包含虚子树),\({\text{siz}_2}_u\) 表示 \(u\) 的所有虚子树大小之和
maintain 时,将 \({\text{siz}_{\text{ch}_{u, 0/1}}}, {\text{siz}_2}_u\) 贡献到 \(\text{siz}_u\) 上
考虑实时维护 \({\text{siz}_2}_u\),分析各个操作的影响:
-
rot, splay,只改变了 splay 中节点的相对位置,不影响虚子树大小 -
access,每次从下层 splay 跳上来时,会将当前点右儿子的边变虚,将下层 splay 连向当前点的边变实因此,令 \({\text{siz}_2}_u \leftarrow {\text{siz}_2}_u + \text{siz}_{\text{ch}_{u, 1}} - \text{siz}_{\text{lst}}\)
-
makeRoot,只调用前面的函数,不影响虚子树大小 -
link,多连了 \(1\) 条虚边,先makeRoot(v), splay(v)后令 \({\text{siz}_2}_v \leftarrow {\text{siz}_2}_v + \text{siz}_u\)注意,必须先
makeRoot(v), splay(v),这样 \(v\) 为辅助树的根,没有对上层节点的影响 -
cut,在makeRoot, access后将要断边调整为实边,断实边不影响虚子树大小
查询时,先把 \((x, y)\) 断掉,统计答案后再连回来即可
时间复杂度 \(O(q \log n)\)
要求:
-
若按照上述方法直接维护,要求信息可加可减
-
反之 (如 \(\max, \min\) ),考虑对每个点用 set 维护,只需插入 / 删除虚子树 \(\max\) / \(\min\) 即可
拓展:对 \(\text{kthmax}\),考虑权值线段树套 LCT,外层线段树上 \([l, r]\) 只维护点权在 \([l, r]\) 中的点的连通性
查询时,在外层线段树上二分,check 即判断 \(u\) 所在连通块大小与 \(k\) 的大小关系
显然 \(1\) 个点至多拆到 \(\log n\) 个外层线段树区间上,内层 LCT 可以直接建,总点数 \(O(n \log n)\)
时间复杂度 \(O(n \log n \log V)\)
*P2387 [NOI2014] 魔法森林 (LCT 维护生成树 + 拆边)
给定 \(n\) 个点 \(m\) 条边的无向图,第 \(i\) 条边 \(e_i\) 有两个边权 \(a_i, b_i\)
找到 \(1\) 到 \(n\) 的路径 \(P\),最小化 \(\max_{e_i \in P} a_i +\max_{e_i \in P} b_i\),求这个值;若无法从 \(1\) 到达 \(n\),输出
-1\(2 \le n \le 5 \cdot 10^4\),\(0 \le m \le 10^5\),\(1 \le a_i, b_i \le 5 \cdot 10^4\);3s, 125MB
[!NOTE]
按 \(a_i\) 排序,枚举 \(a_i\),限制只能用 \(e_{[1, i]}\) \(\to\) 加 \(e_i = (u_i, v_i)\) 时,找到生成树上 \(u_i \rightsquigarrow v_i\) 间的最大 \(b_i\) 并尝试删去
[!IMPORTANT]
Trick1:LCT 维护最小生成树,对这种两个限制的,固定 \(1\) 维,维护另 \(1\) 维
Trick2:拆边 ( \((u, v) \to (u, \text{mid}), (\text{mid}, v)\) ) 刻画边,将边权挂在 \(\text{mid}\) 上
本质上,在 \(a_i\) 已经固定时,我们希望最小化 \(\max_{e_i \in P} b_i\),因此只要 \(b_i\) 优就加入,能 "替代" 被顶掉的边必定不劣
令 \(n, m\) 同阶,时间复杂度 \(O(n \log n)\)
*P5385 [Cnoi2019] 须臾幻境 (转生成森林边数 + LCT 维护生成森林,替换最前边 + 主席树维护出现次数)
给定 \(n\) 个点 \(m\) 条边的无向图,令边集 \(E = (e_1, e_2, \cdots, e_m)\),其中 \(e_i = (u_i, v_i)\)
\(q\) 次询问,第 \(i\) 次给定 \(l_i, r_i\),求若只保留 \(e_{l_i}, e_{l_i+1}, \cdots, e_{r_i}\),图中的连通块个数
强制在线;\(1 \le n \le 10^5\),\(1 \le m \le 2 \cdot 10^5\),\(1 \le q \le 10^5\);2s, 500MB
[!NOTE]
连通块个数转生成森林边数 \(\to\) LCT 维护生成森林,每次删掉最前边 \(\to\) 主席树维护 \(1 \sim r\) 时刻边出现次数
[!IMPORTANT]
Trick:连通块个数转生成森林边数
设生成森林边数为 \(c\),显然连通块个数为 \(n-c\)
考虑 LCT 维护生成森林,为保证 \(l \sim r\) 的生成森林是 \(1 \sim r\) 的生成森林的子集,每当成环时,删去环上最靠前的边,加当前边
对区间询问,考虑主席树,对每个时刻 \(r\) 记录 \(1 \sim r\) 时 \([l', r']\) 中存在边的个数,变化量 \(O(1)\),可以接受
令 \(n, m, q\) 同阶,时间复杂度 \(O(n \log n)\)
**UOJ207 - 共价大爷游长沙 (异或哈希 + 固定根,LCT 维护子树信息 - 单点异或,子树异或和)
给定 \(n\) 个点的树,有点对集合 \(S\),初始为空
称边 \((u, v)\) 合法,当且仅当 \(\forall (x, y) \in S\),\((u, v)\) 在 \(x \rightsquigarrow y\) 的树上路径上
有 \(m\) 次操作,第 \(i\) 次形如以下四种:
- 给定 \(x, y, u, v\),删边 \((x, y)\),连边 \((u, v)\),保证操作后的图还是树
- 给定 \(x, y\),在 \(S\) 中加入点对 \((x, y)\)
- 给定 \(\text{id}\),删除第 \(\text{id}\) 个加入 \(S\) 的点对
- 给定 \(x, y\),询问边 \((x, y)\) 是否合法
\(n \le 10^5\),\(m \le 3 \cdot 10^5\);2s, 512MB
[!NOTE]
刻画限制,所有 \((x_i, y_i)\) 恰有 \(1\) 个在 \(u\) 子树中 \(\to\) 维护一万个信息,考虑异或哈希 \(\to\) LCT 维护动态问题
[!IMPORTANT]
Trick1:异或哈希维护一大堆东西的出现情况
Trick2:固定根,LCT 维护子树信息
显然,边 \((u, \text{fa}_u)\) 合法 \(\iff\) 对每个点对 \((x_i, y_i) \in S\),\(x_i, y_i\) 恰有 \(1\) 个在 \(u\) 子树中
考虑异或 hash,给每个点对 \((x_i, y_i)\) 赋随机权 \(w_i\)
因此,在 \(S\) 中加入 / 删除 \((x, y)\) 等价于对 \(x, y\) 单点异或
查询时,即求子树异或和,判断是否与全局异或和相等
可以 LCT 维护,令 \(n, m\) 同阶,时间复杂度 \(O(n \log n)\)
以读错题的版本 (到根链整体异或,子树查) 为例,固定根时,LCT 维护子树信息的操作:
-
此时,我们不能随便
makeRoot,否则会破坏原有子树结构 -
在每个点,对树信息,维护
siz, siz1, siz2,表示 splay 上子树大小 (包含虚子树),实子树大小,虚子树大小对值信息,维护
v, s, s2, t,表示点权,子树异或和 (包含虚子树),虚子树异或和,异或 tag为什么维护
siz1?整体异或时,不会对虚子树异或,更改s的条件是siz1 & 1而非siz & 1为什么维护
v?pushup时,需要补上当前点权 (例:只有 \(1\) 个点时,不记v就会导致pushup时总是 \(0\) ) -
对
access,正常维护虚子树信息即可 -
对
link,不能直接makeRoot, 必须先find判断 \((u, v)\) 中谁是父亲 (和根相连)令 \(v = \text{fa}_u\),需要先
access(v), splay(v),让 \(v\) 在整个辅助树的最顶端,这样不用考虑对 \(v\) 祖先的贡献进行这两步操作后,正常把 \(u\) 挂到 \(v\) 上,更新虚子树信息即可
-
对
cut,同理,不能makeRoot,但是又不能直接find判断 \((u, v)\) 中谁是父亲考虑类似
find的思路,钦定 \(v\) 是父亲,check 即找到 \(u\) 的左儿子,一直往右子树走,看最后走到的是不是 \(v\)确定父子关系后,将 \(u\) 转到辅助树树根,双向断开 \(u\) 和左儿子即可
常见误区:以 \(u\) 为根,\(v\) 未必就是 \(u\) 的左儿子,不能这么判父子关系
-
对查询操作,考虑先
access(u), splay(u),将 \(u\) 转到辅助树根,这样就考虑到了之前的修改此时,\(u\) 左子树在原树上是 \(u\) 的祖先,不应考虑进来;由于
access(u),此时 \(u\) 没有右子树综上,答案就是虚子树异或和 \(\oplus\) 当前点权
本质上,我们利用 splay 上虚子树和原树的对应关系查询答案,没有用 splay 中的子树结构 (直接查不能处理懒标记)
*1.6 T2 - match (建树 + LCT)
有 \(1\) 场比赛,有 \(n\) 个选手参加,依次编号为 \(0, 1, \cdots, n-1\)
初始时,数字 \(x = 0\),第 \(i\) 个选手有属性 \(a_i\)
比赛开始后,按 \(0, 1, \cdots, n-1\) 的顺序,选手依次决定是否令 \(x \leftarrow (x+a_i) \bmod n\);最终,\(x\) 就是胜者的编号
每个选手绝顶聪明,第 \(i\) 个选手决定操作,当且仅当他操作后会赢,且不操作不会赢;所有选手都知道这个信息
有若干局,每局开始前给定 \(x, y\),将 \(a_x\) 修改为 \(y\),询问每局的赢家;修改永久有效
\(1 \le n, q \le 3 \cdot 10^5\),\(0 \le a_i \le n-1\);3s, 512MB
非说是 LCT 板子也没啥办法是吧
要意识到,因为自己菜而生气,是毫无用处的
[!NOTE]
从后向前做 \(\to\) 同时维护 \(n\) 个 DP 值,做不了
从前向后做 \(\to\) 建树刻画胜利条件 \(\to\) 维护黑白点,修改相当于链翻转 \(\to\) LCT,splay 上二分找修改分界点,判据为子树内无虚黑儿子
考虑从前向后做,对第 \(i\) 个选手,容易发现 \(0 \sim i-1\) 不会把 \(x\) 改到 \(\ge i\)
令 \(p_i = (i-a_i) \bmod n\),当轮到 \(i\) 且 \(x = p_i\) 时,\(i\) 可能把 \(x\) 变成 \(i\) (容易发现,其他时候不会改)
若 \(p_i < i\),连边 \(i \to p_i\);称点 \(i\) 为 "黑点",当且仅当改成 \(i\) 后能保持住,不被后面改掉,"白点" 则表示保持不住
容易发现,点 \(u\) 为黑点,当且仅当所有 \(v \in \text{son}_u\) 都是白点,这样 \(v\) 不会改掉 \(u\)
对答案,若 \(0\) 为黑点,则答案为 \(0\),反之为 \(0\) 的儿子中编号最小的黑点
每次修改相当于断父边 + 连新父边,考虑 LCT
注意,所有树始终是根向的,方向为编号大 \(\to\) 编号小,不会有换根操作
以连边 \((u, \text{fa}_u)\) 为例,分析修改的影响:
-
若 \(u, \text{fa}_u\) 有白点,分讨下可得没有影响
-
反之,\(\text{fa}_u\) 的颜色会翻转,\(\text{fa}_{\text{fa}_u}\) 也翻转,当且仅当其仅有 \(\text{fa}_u\) \(1\) 个黑儿子
一直往上走,最终是链翻转,可以 splay 上二分找分界点,容易发现判据是 splay 子树内没有原树上虚黑儿子
具体的,由于 splay 高度有保证,维护子树内原树虚黑儿子个数,直接往左 / 右儿子走 + check 即可
-
翻转原树上链时,容易发现,只会对原树链顶的 \(\text{fa}\) 的虚黑儿子个数产生影响
-
特别的,对 \(0\) 单独开 set,维护虚黑儿子编号
查询时,
access(0)即可
问题在于,维护原树信息,不能在切换虚实边时直接改,因为 splay 上虚边父子 \(\ne\) 原树上虚边父子
这里直接暴力找原树儿子 (类比 find,不断向左子树跳),每次 splay 下,复杂度就是对的
视 \(n, q\) 同阶,时间复杂度 \(O(n \log n)\)
线性基
*P4151 [WC2011] 最大XOR和路径 (链套环 - 链随便,环独立 + 线性基维护环)
给定 \(n\) 个点 \(m\) 条边的无向图,边有边权,求 \(1\) 条路径,使得经过的边的边权异或和最大
若 \(1\) 条边被经过多次,其边权也应贡献多次
\(n \le 5 \cdot 10^4\),\(m \le 10^5\),\(V \le 10^{18}\);1s, 500MB
[!NOTE]
考虑拆成链 + 环,环与链共享恰好 \(1\) 个顶点 \(\to\) 初始链随便选,因为若其他链更优,异或上环后可以变过去 \(\to\) 线性基维护环
[!IMPORTANT]
Trick:路径拆成链 + 环
实现上,由于初始链随便选,可以直接 dfs,搜到环就加进线性基,最终对 \(\text{dis}_n\) 查 \(\max\) 即可
*CF1299D - Around the World (dfs 树上考虑 + 内部环独立,分讨 1 环 + 对线性基 DP)
给定 \(n\) 个点 \(m\) 条边的简单无向连通图,第 \(i\) 条边有边权 \(w_i\),保证不存在长度大于 \(3\) 的简单环经过 \(1\)
路径的权定义为经过的边的异或和,若 \(1\) 条边被经过多次,其边权也应算多次
你可以选择任意一组与 \(1\) 相连的边,并将它们删除
求有多少种删边方案,使得删除后,图中不存在经过 \(1\) 的权为 \(0\) 的非平凡环 (经过某条边奇数次)
答案对 \(10^9+7\) 取模
\(1 \le n, m \le 10^5\),\(0 \le w_i < 32\);2s, 500MB
[!NOTE]
刻画判定 \(\to\) dfs 生成树上考虑,\(1\) 各儿子独立 \(\to\) 内部环独立,与 \(1\) 成环只能成三元环 \(\to\) 分讨 + 线性基
刻画删边 \(\to\) \(w_i < 2^5\),打表发现线性基很少 \(\to\) 分讨删边情况,对线性基 DP
[!IMPORTANT]
Trick:对消元后的线性基 DP
考虑 dfs 生成树,由于没有横叉边,各个儿子独立
将 \(1\) 删去,对每个连通块,手玩下,发现内部的环是独立的,可以全插到线性基里维护
对连 \(1\) 边,容易发现最多 \(2\) 条,且若为 \(2\) 条则必定构成三元环
由 \(w_i < 2^5\),打表发现,消元后线性基只有 \(374\) 个,可以直接对线性基 DP
记 \(f_{i, \text{id}}\) 表示考虑前 \(i\) 个儿子,生成 \(\text{id}\) 这个线性基的方案数,转移时分讨下连 \(1\) 边怎么删,特判内部不合法即可
预处理线性基合并,容易做到 \(O(1)\) 转移
记线性基个数为 \(B\),时间复杂度 \(O(BV \log V + B^2 \log V + nB)\)
动态 DP
P4751 【模板】动态 DP(加强版)
给定 \(n\) 个点的树,点有点权,求树上最大带权独立集
有 \(m\) 次操作,每次会修改 \(1\) 个点的点权,每次操作后输出答案
强制在线;\(n \le 10^6\),\(m \le 3 \cdot 10^6\);3.5s, 250MB
若不带修,有经典 DP 是 \(f_{u, 0/1}\) 表示 \(u\) 不选 / 选,\(u\) 子树内答案
对 \(1\) 次修改,显然只会影响到根链上的 DP 值
考虑重剖,到根链上至多经过 \(\log\) 条轻边,这启示我们设计与轻重儿子有关的状态
令 \(g_{u, 0/1}\) 表示 \(u\) 不选 / 选,不考虑重儿子子树,\(u\) 子树内答案
有转移 \(f_{u, 0} \leftarrow g_{u, 0} + \max(f_{\text{son}_u, 0}, f_{\text{son}_u, 1})\),\(f_{u, 1} \leftarrow g_{u, 1} + f_{\text{son}_u, 0}\)
显然,重链内 \(1\) 点的修改,不影响重链内的 \(g_{u, 0/1}\) 值,只会在切换重链时单点修 \(1\) 次
考虑合并 \(g_{v, 0/1}\) 对 \(f_{u, 0/1}\) 的贡献以快速求 \(f_{u, 0/1}\)
将转移写成 \((\max, +)\) 矩阵形式,即 \(\begin{bmatrix} g_{u, 0} & g_{u, 0} \\ g_{u, 1} & -\infty \end{bmatrix} \begin{bmatrix} f_{\text{son}_u, 0} \\ f_{\text{son}_u, 1} \end{bmatrix} = \begin{bmatrix} f_{u, 0} \\ f_{u, 1} \end{bmatrix}\)
上线段树维护重链区间矩阵积,\(1\) 次修改相当于 \(\log\) 次单点改,查询直接用根所在重链的矩阵乘上 \(\begin{bmatrix} 0 \\ 0 \end{bmatrix}\)
时间复杂度 \(O(n \log^2 n)\)
可以上全局平衡二叉树 (GBST),做到 \(O(n \log n)\) :
-
GBST 类似 LCT,但它的结构是确定的
对每个重链,开 \(1\) 个 BST 维护,满足该 BST 的中序遍历为重链按 \(\text{dep}\) 从小到大遍历的结果
对每个 BST,其根节点的 \(\text{fa}\) 指向原树上重链顶的父亲,同样儿子认父亲,父亲不认儿子
-
建树时,令点 \(u\) 的权 \(w_u\) 为其轻子树大小之和 \(+1\),本质上是它对子树大小的贡献
记 \(w_u\) 的前缀和数组为 \(s_u\)
设考虑区间 \([l, r]\),找到区间的加权中点 (第 \(1\) 个满足 \(2(s_u-s_{l-1}) > s_r-s_{l-1}\) 的点 \(u\) )
为什么?这样保证从实儿子跳上来时,子树大小翻倍,BST 高度是 \(\log\) 的
跳虚边时,容易发现子树大小不降,实边高度 \(\log n\) 的证明仍成立
对虚边高度,由于跳 \(1\) 次相当于切换重链,易得虚边高度也是 \(\log\) 的
综上,总高度是 \(\log\) 的
-
本题中,需要维护 BST 上 (实) 子树矩阵积,表示重链整体的矩阵积
单点改点 \(u\) 时,只会对 BST 上 \(u\) 到根链有影响,向上跳实边时 pushup
跳虚边时,先求出到当前重链顶端的 \(f_{u, 0/1}\),对切换重链后跳到的位置单点改,重复上述操作即可
P.S. 差不多的题:P5024 [NOIP 2018 提高组] 保卫王国 & P6021 洪水
可并堆 / 配对堆
配对堆
是一种可并堆
堆结构:对 \(u\) 的每个儿子 \(v_i\),只记录第 \(1\) 个儿子 \(v_1\),其他儿子挂到 \(v_1\) 的兄弟链上,即 \(v_1 \to v_2 \to \cdots \to v_i\)
基础操作:
-
int meld(int u, int v),合并以 \(u, v\) 为根的两堆,返回合并后根节点以小根堆为例,不妨设 \(w_u \le w_v\), 将 \(v\) 挂到 \(u\) 儿子的兄弟链最前端,再把 \(u\) 的第 \(1\) 儿子置为 \(v\) 即可
设原结构为 \(v_1 \to u, v_1 \to v_2 \to \cdots v_i\),则现结构为 \(v \to u, v \to v_1 \to \cdots \to v_i\)
-
int merges(int u),将 \(u\) 右侧的兄弟链全部合并到 \(u\) 子树里,返回合并后根节点设原兄弟链为 \(u \to v_1 \to v_2 \to \cdots \to v_i\)
流程为,先将相邻两儿子
meld起来,变为 \((u, v_1) \to (v_2, v_3) \to \cdots \to (v_{i-1}, v_i)\)接下来,从右向左挨个
meld,即先合并 \((v_{i-3}, v_{i-2}), (v_{i-1}, v_i)\),再把合并后的堆与 \((v_{i-5}, v_{i-4})\) 合并,以此类推实现上,找到 \(v_1, v_2\),断开兄弟边,做
meld(meld(u, v1), merges(v2))即可 -
int pop(int u),删堆顶,返回根节点把 \(u\) 的儿子兄弟链
merges起来,再把 \(u\) 扔掉即可
复杂度:
- 分析 \(1\):均摊 \(O(1)\)
meld,\(O(\log n)\)pop - 分析 \(2\):均摊 \(O(2^{2 \sqrt{\log \log n}})\)
meld和pop
*P3261 [JLOI2015] 城池攻占 (配对堆打 tag)
有 \(m\) 个骑士,攻打 \(n\) 个城池;这 \(n\) 个城池形成树结构
第 \(i\) 个城池有防御力 \(h_i\),属性 \(a_i \in \{0, 1\}, v_i\)
第 \(i\) 个骑士有战斗力 \(s_i\),初始攻打城池 \(c_i\)
对城池 \(u\) 与骑士 \(i\),\(i\) 能 "攻下" \(u\),当且仅当 \(s_i \ge h_u\),反之,骑士会在此 "战死"
攻下后 \(u\),若 \(a_u = 0\),则 \(s_i \leftarrow s_i + v_u\),\(a_u = 1\) 则 \(s_i \leftarrow s_i \cdot v_u\);之后,未战死的骑士会去攻打 \(\text{fa}_u\)
对每个城池,求多少个骑士在此战死;对每个骑士,求他攻下了多少个城池
\(1 \le n, m \le 3 \cdot 10^5\),\(-10^{18} \le h_i, v_i, s_i \le 10^{18}\),\(a_i \in \{0, 1\}\);1s, 256MB
[!NOTE]
\(1\) 个骑士只会战死 \(1\) 次 \(\to\) 小根堆维护,向父亲走时合并堆,打加法 tag + 乘法 tag
容易发现,每次 meld 时需要 pushdown,但遍历儿子的每个兄弟 pushdown 复杂度是错的
一个想法是先 merges 再 pushdown,但 merges 需要 meld,会循环调用,把整个堆都遍历 \(1\) 遍
考虑维护 \(2\) 个 tag,\(1\) 个向子树传,\(1\) 个向右侧兄弟传
meld 时,下传子树标记,正常做即可
merges 时,设当前合并 \(u \to v_1 \to v_2\),下传 \(u\) 的子树标记,再将 \(u\) 的兄弟标记传给 \(v_1\),\(v_1\) 的兄弟标记传给 \(v_2\)
pop 时,下传子树标记,正常做即可

浙公网安备 33010602011771号