一些技巧与思想也会归类于数据结构。
D - 数据结构 序列结构
树状数组
\(\mathrm{lowbit}(x)\) 函数:表示 \(x\) 的二进制表示中,最低位的 \(1\) 的数值大小,lowbit(x) = x & -x
。
树状数组 \(c_i\) 的管辖区间:\([i - \mathrm{lowbit}(i) + 1, i]\)。
\(\mathcal{O}(n)\) 建树:
- 方法一:每一个节点的值是由所有与自己直接相连的儿子的值求和得到的,因此可以倒着考虑贡献。每次确定完 \(c_i\) 的值后,用 \(c_i\) 的值更新 \(c_{i + \mathrm{lowbit}(i)}\)。
- 方法二:预处理前缀和数组,则 \(c_i = \mathrm{pre}_i - \mathrm{pre}_{i - \mathrm{lowbit}_i}\)。
\(\mathcal{O}(\log n)\) 查询第 \(k\) 小:树状数组维护每个数的出现次数,倍增求解。
线段树
- 任意区间 \([l, r]\) 在线段树上,至少需要 \(\mathcal{O}(\log n)\) 个节点覆盖。
动态开点线段树 & 线段树合并 & 线段树分裂
- 动态开点线段树,单次插入新建节点个数 \(\mathcal{O}(\log A)\)。其中 \(A\) 表示值域大小。
- 动态开点线段树可以使用懒标记,下传标记时需要新建结点(若标记对空节点无影响则不必新建节点)。
- 遇到仅针对某个权值的修改和询问时,可以考虑对每个权值开一个动态开点线段树。
- 线段树合并单次复杂度,取决于两棵线段树的交集大小;线段树合并总复杂度,取决于所有线段树的节点总数。
- 线段树合并支持持久化,需要在合并时新建节点。
- 在线段树合并的过程中,可以统计来自不同线段树且具有相对顺序关系的信息。例如逆序对。
- 在线段树合并的过程中,在处理复杂的问题时,可以尝试分成以下几类讨论(其中 2, 3 是讨论重点):
- (1)\(p, q\) 均为空节点:返回空节点。
- (2)\(p, q\) 中分别有一个空节点和一个非空节点:特殊讨论。
- (3)线段树递归到叶子节点(即 \(l = r\)):特殊讨论。
- (4)\(p, q\) 均为非空节点:分别合并 \(p, q\) 的左儿子与 \(p, q\) 的右儿子,最后 update。
- 线段树分裂单次复杂度 \(\mathcal{O}(\log A)\),新建节点个数 \(\mathcal{O}(\log A)\)。
namespace SGT {
const int pond = ...;
int nClock, root[N];
struct node {
int lc, rc;
int cnt;
} t[pond];
int create() {
int p = ++ nClock;
t[p].lc = t[p].rc = t[p].cnt = 0;
return p;
}
void upd(int p) {
t[p].cnt = t[t[p].lc].cnt + t[t[p].rc].cnt;
}
void insert(int &p, int l, int r, int x, int val) {
if (!p) p = create();
t[p].cnt += val;
if (l == r) return;
int mid = (l + r) >> 1;
if (x <= mid)
insert(t[p].lc, l, mid, x, val);
else
insert(t[p].rc, mid + 1, r, x, val);
}
int merge(int p, int q) {
if (!p || !q) return p ^ q;
t[p].cnt += t[q].cnt;
t[p].lc = merge(t[p].lc, t[q].lc);
t[p].rc = merge(t[p].rc, t[q].rc);
return p;
}
/* 可持久化线段树合并
int merge(int p, int q) {
if (!p || !q) return p ^ q;
int u = ++ nClock;
t[u].cnt = t[p].cnt + t[q].cnt;
t[u].lc = merge(t[p].lc, t[q].lc);
t[u].rc = merge(t[p].rc, t[q].rc);
return u;
}
*/
void split_v(int p, int l, int r, int val, int &x, int &y) {
if (!p)
x = y = 0;
else {
int mid = (l + r) >> 1;
if (mid < val)
x = p, y = create(), split_v(t[p].rc, mid + 1, r, val, t[x].rc, t[y].rc);
else if (mid == val)
x = create(), y = create(), t[x].lc = t[p].lc, t[y].rc = t[p].rc;
else
x = create(), y = p, split_v(t[p].lc, l, mid, val, t[x].lc, t[y].lc);
upd(x), upd(y);
}
}
void split_s(int p, int sze, int &x, int &y) {
if (!p)
x = y = 0;
else {
if (t[t[p].lc].cnt < sze)
x = p, y = create(), split_s(t[p].rc, size - t[t[p].lc].cnt, t[x].rc, t[y].rc);
else if (t[t[p].lc].cnt == sze)
x = create(), y = create(), t[x].lc = t[p].lc, t[y].rc = t[p].rc;
else
x = create(), y = p, split_s(t[p].lc, size, t[x].lc, t[y].lc);
upd(x), upd(y);
}
}
}
主席树
- 主席树可以作为线段树的前缀和来使用。
- 主席树区间修改,可以标记永久化。
namespace SGT {
int pond = ...;
int nClock, root[N];
struct node {
int lc, rc;
int cnt;
} t[pond];
void insert(int &p, int q, int l, int r, int x, int val) {
p = ++ nClock, t[p] = t[q];
t[p].cnt += val;
if (l == r) return;
int mid = (l + r) >> 1;
if (x <= mid)
insert(t[p].lc, t[q].lc, l, mid, x, val);
else
insert(t[p].rc, t[q].rc, mid + 1, r, x, val);
}
}
namespace SGT {
const int pond = ...;
int nClock, root[N];
struct node {
int lc, rc;
int sum;
int add;
} t[pond];
void change(int &p, int q, int l, int r, int s, int e, int val) {
p = ++ nClock, t[p] = t[q];
if (s <= l && r <= e) { t[p].add += val; return; }
t[p].sum += (std::min(r, e) - std::max(l, s) + 1) * val;
int mid = (l + r) >> 1;
if (s <= mid)
change(t[p].lc, t[q].lc, l, mid, s, e, val);
if (mid < e)
change(t[p].rc, t[q].rc, mid + 1, r, s, e, val);
}
int ask(int p, int l, int r, int s, int e) {
if (s <= l && r <= e)
return t[p].sum + (r - l + 1) * t[p].add;
int mid = (l + r) >> 1;
int cur = (std::min(r, e) - std::max(l, s) + 1) * t[p].add;
if (s <= mid)
cur += ask(t[p].lc, l, mid, s, e);
if (mid < e)
cur += ask(t[p].rc, mid + 1, r, s, e);
return cur;
}
}
势能线段树
- 势能线段树使用条件:修改操作会使得被修改元素的势能迅速向零势能逼近。换言之、每个元素存在被有效修改次数的上限。
- 在区间修改时,暴力往叶子节点递归。若递归途中遇到某个节点,该节点的对应区间内每个元素都不能进行有效修改,则在该节点
return
。 - 在无修改操作时,时间复杂度为 \(\mathcal{O}(n\alpha\log n)\)。其中 \(\alpha\) 为有效修改次数上限。
区间开平方:一个正整数 \(x\) 被开方 \(\mathcal{O}(\log \log x)\) 次后会变成 \(1\)。若区间最大值 \(= 1\),则 return
。
区间取模:一个整数 \(x\) 对 \(p\) 取模,若 \(x \geq p\),则 \(x\) 至少变小一半。若区间最大值 \(< p\),则 return
。
区间除法、区间加:整除一个数 \(p\),会使区间最大值与最小值的差至少变小一半。
- 若 \(\max - \min = 0\),则打加法标记。
- 若 \(\max - \min=1\) 且 \(\left\lfloor\frac{\max}{p}\right\rfloor \neq \left\lfloor\frac{\min}{p}\right\rfloor\),则打加法标记。
区间按位与:一个二进制数 \(x\) 对 \(v\) 进行按位与,相当于是把所有 \(v\) 为 \(0\) 的位,在 \(x\) 中强制置为 \(0\)。若 \(v\) 为 \(0\) 的位置集合被区间按位或和为 \(0\) 的位置集合包含,则 return
。
区间取最值(吉司机线段树):以区间取最小值为例。
- 在线段树上的每一个节点 \(p\),需要维护:
- \(\max\):区间最大值。
- \(\mathrm{cnt}\):区间最大值的个数。
- \(\mathrm{smax}\):区间严格次大值。
- 在区间修改时,若递归途中遇到一个被询问区间完全包含的节点 \(p\) 时:
- 若 \(x \geq \max_p\),则
return
。 - 若 \(x \leq \mathrm{smax}_p\),则暴力往左右儿子递归。
- 若 \(\mathrm{smax}_p < x < \max_p\),则有 \(\mathrm{cnt}\) 个 \(\max_p\) 被改为 \(x\)。打标记即可。
- 若 \(x \geq \max_p\),则
- 无区间加时,时间复杂度 \(\mathcal{O}(n \log n)\)。
- 有区间加时,时间复杂度 \(\mathcal{O}(n \log^2 n)\)。
历史线段树
猫树
- 建猫树时,采用堆式建树(节点 \(p\) 的左、右儿子分别为 \(2p\) 与 \(2p + 1\))。具体实现时不必显式地将左右儿子的结构建出来,只需记录每一层深度的具体信息,与每个元区间 \([l, l]\) 对应的叶子节点编号即可。
- 若将序列长度补成 \(2\) 的整数次幂:不难发现左儿子编号为父节点编号左移一位,右儿子编号为父节点编号左移一位再加一。
- 故 \([l, l]\) 与 \([r, r]\) 的代表节点 \(x, y\) 的 LCA 即为 \(x, y\) 二进制下从高位向低位的 LCP。
- 若 \([l, l]\) 与 \([r, r]\) 的代表节点为 \(x, y\),则 LCA 编号为
x >> logx[x ^ y]
,深度为logx[x] - logx[x ^ y]
。特别要注意的是logx[0] = 0
。
namespace SGT {
int dep[N * 4];
int idx[N];
int f[logN][N];
void build(int p, int l, int r) {
dep[p] = dep[p / 2] + 1;
int mid = (l + r) >> 1;
// [l, mid] ...
// [mid + 1, r] ...
if (l == r) idx[l] = p;
else build(p * 2, l, mid), build(p * 2 + 1, mid + 1, r);
}
int ask(int l, int r) {
int x = idx[l], y = idx[r];
if (dep[x] > dep[y]) std::swap(x, y);
while (dep[x] < dep[y]) y /= 2;
while (x ^ y) x /= 2, y /= 2;
return ...;
}
}
namespace SGT {
const int H = ...;
int h, logx[H];
int idx[H];
int f[logH][H];
void init() {
h = 1;
while (h < n) h <<= 1;
logx[0] = 0;
for (int i = 1; i <= h; i ++) logx[i] = logx[i >> 1] + 1;
}
void build(int p, int l, int r, int dep) {
int mid = (l + r) >> 1;
// [l, mid] ...
// [mid + 1, r] ...
if (l == r) idx[l] = p;
else build(p * 2, l, mid, dep + 1), build(p * 2 + 1, mid + 1, r, dep + 1);
}
int ask(int l, int r) {
int x = idx[l], y = idx[r];
int z_dep = logx[x] - logx[x ^ y];
return ...;
}
}
李超线段树
楼房重建式线段树
参考:https://www.cnblogs.com/PinkRabbit/p/Segment-Tree-and-Prefix-Maximums.html
写法 1:当需要维护的信息满足可减性时,可以考虑采用写法 1。
-
在线段树上的每一个节点 \(p\),需要维护:
- \(\max[p]\):区间最大值。
- \(\mathrm{dat}[p]\):区间内所有前缀最大值的信息和。
-
定义函数 \(\mathrm{calc}(p, \mathrm{pre})\) 表示以 \(\mathrm{pre}\) 为初始的前缀最大值时,区间内所有前缀最大值的信息和。
- 若 \(p\) 为叶节点,则 \(\mathrm{calc}(p, \mathrm{pre}) = [\mathrm{pre} < \mathrm{max}[p]]\)。
- 若 \(\mathrm{pre} < \max[\mathrm{lc}[p]]\),此时考虑完左子树后前缀最大值必定为 \(\max[\mathrm{lc}[p]]\),而右子树的信息和可以通过做差得到。
- 若 \(\mathrm{pre} \geq \max[\mathrm{lc}[p]]\),此时左子树不会出现前缀最大值,只需考虑右子树即可。
-
update 时,有 \(\mathrm{dat}[p] = \mathrm{dat}[\mathrm{lc}[p]] + \mathrm{calc}(\mathrm{rc}[p], \max[\mathrm{lc}[p]])\)。
写法 2:当需要维护的信息不满足可减性时,可以考虑采用写法 2。
-
在线段树上的每一个节点 \(p\),需要维护:
- \(\max[p]\):区间最大值。
- \(\mathrm{dat}[p]\):以左子树的区间最大值为初始的前缀最大值时,右子树内所有前缀最大值的信息和。特别地,叶子无定义。
-
定义函数 \(\mathrm{calc}(p, \mathrm{pre})\) 表示以 \(\mathrm{pre}\) 为初始的前缀最大值时,区间内所有前缀最大值的信息和。
- 若 \(p\) 为叶节点,则 \(\mathrm{calc}(p, \mathrm{pre}) = [\mathrm{pre} < \mathrm{max}[p]]\)。
- 若 \(\mathrm{pre} < \mathrm{max}[\mathrm{lc}[p]]\),同上
- 若 \(\mathrm{pre} \geq \max[{\mathrm{lc}[p]}]\),同上
-
update 时,有 \(\mathrm{dat}[p] = \mathrm{calc}(\mathrm{rc}[p], \max[\mathrm{lc}[p]])\)。
平衡树
splay
- splay 核心思想:每次操作完后,通过双旋 & 单旋将相关节点 \(x\) 旋转至根。
- splay 的三种旋转:
- 当 \(x\) 的父亲 \(p\) 是根节点时,直接 zig/zag \(x\)。
- 当 \(x\) 和上两代祖先位于一条链上:先 zig/zag \(p\),再 zig/zag \(x\)。
- 当 \(x\) 和上两代祖先分叉时:先 zig/zag \(x\),再 zig/zag \(x\)。
- splay 基于均摊分析,不支持持久化。
- splay 启发式合并的时间复杂度 \(\mathcal{O}(n \log n)\)(splay 是 Dynamic Finger Search Tree,需要保证第二棵树按中序遍历的顺序插入)。
namespace BST {
int nClock, root;
struct splay {
int son[2], Fa;
int val;
int sze;
bool rev;
#define lc son[0]
#define rc son[1]
void mk_rev() {
std::swap(lc, rc);
rev ^= 1;
}
} t[N];
int create(int p, int val) {
int x = ++ nClock;
t[x].lc = t[x].rc = 0, t[x].Fa = p;
t[x].val = val;
t[x].sze = 1;
t[x].rev = 0;
return x;
}
void upd(int p) {
t[p].sze = t[t[p].lc].sze + t[t[p].rc].sze + 1;
}
void spread(int p) {
if (t[p].rev) {
if (t[p].lc) t[t[p].lc].mk_rev();
if (t[p].rc) t[t[p].rc].mk_rev();
t[p].rev = 0;
}
}
int dir(int x) { return t[t[x].Fa].rc == x; }
void rotate(int x) {
int y = t[x].Fa, z = t[y].Fa, k = dir(x);
t[x].Fa = z; if (z) t[z].son[t[z].rc == y] = x;
t[y].son[k] = t[x].son[k ^ 1], t[t[x].son[k ^ 1]].Fa = y;
t[x].son[k ^ 1] = y, t[y].Fa = x;
upd(y), upd(x);
}
void splay(int x, int target) {
int p;
while (p = t[x].Fa, p != target) {
if (t[p].Fa != target) rotate(dir(x) == dir(p) ? p : x);
rotate(x);
}
if (!target) root = x;
}
void insert(int val) {
int x = root, p = 0;
while (x) p = x, x = t[x].son[val > t[x].val];
x = New(p, val), t[p].son[val > t[p].val] = x;
splay(x, 0);
}
int Get_rk(int k) {
int x = root, ans = 0;
while (x) {
spread(x);
if (t[t[x].lc].sze >= k) x = t[x].lc;
else if (t[t[x].lc].sze + 1 == k) { ans = x; break; }
else k -= t[t[x].lc].sze + 1, x = t[x].rc;
}
return ans;
}
}
FHQ treap
- treap 的每个节点有两个关键字 \(\mathrm{val}\) 与 \(\mathrm{dat}\),其中数值 \(\mathrm{val}\) 满足 BST 性质,随机键值 \(\mathrm{key}\) 满足 heap 性质。故 treap 为笛卡尔树。
- treap 的随机性,可以保证期望树高为 \(\mathcal{O}(\log n)\),分析同快速排序。
- 当 FHQ treap 用作区间树时,可以用笛卡尔树的建树方式 \(\mathcal{O}(n)\) 建树。
- FHQ treap 的合并操作,不能合并值域有交集的两棵树。
- FHQ treap 支持持久化,需要在分裂时复制节点。涉及到打标记时,需要在标记下传时复制节点。在合并时要用
rand() % (t[p].sze + t[q].sze) < t[p].sze
来代替随机键值大小的判断。 - FHQ treap 可以用持久化的方式复制区间。
namespace BST {
int nClock, root;
struct node {
int lc, rc;
int val, key;
int sze;
bool rev;
void mk_rev() {
std::swap(lc, rc);
rev ^= 1;
}
} t[N];
int create(int val) {
int p = ++ nClock;
t[p].lc = t[p].rc = 0;
t[p].val = val, t[p].key = rand();
t[p].sze = 1;
t[p].rev = 0;
return p;
}
/* 可持久化平衡树
int copy(int q) {
int p = ++ nClock; t[p] = t[q];
return p;
}
*/
void upd(int p) {
t[p].sze = t[t[p].lc].sze + t[t[p].rc].sze + 1;
}
void spread(int p) {
if (t[p].rev) {
if (t[p].lc) t[t[p].lc].mk_rev();
if (t[p].rc) t[t[p].rc].mk_rev();
t[p].rev ^= 1;
}
}
/* 可持久化平衡树
void spread(int p) {
if (t[p].rev) {
if (t[p].lc) t[p].lc = copy(t[p].lc), t[t[p].lc].mk_rev();
if (t[p].rc) t[p].rc = copy(t[p].rc), t[t[p].rc].mk_rev();
t[p].rev = 0;
}
}
*/
void split_v(int p, int val, int &x, int &y) {
if (!p)
x = y = 0;
else {
spread(p);
if (t[p].val <= val)
x = p, split_v(t[p].rc, val, t[x].rc, y);
else
y = p, split_v(t[p].lc, val, x, t[y].lc);
upd(p);
}
}
void split_s(int p, int sze, int &x, int &y) {
if (!p)
x = y = 0;
else {
spread(p);
if (t[t[p].lc].sze < sze)
x = p, split_s(t[p].rc, sze - t[t[p].lc].sze - 1, t[x].rc, y);
else
y = p, split_s(t[p].lc, sze, x, t[y].lc);
upd(p);
}
}
/* 可持久化平衡树
void split_v(int p, int val, int &x, int &y) {
if (!p)
x = y = 0;
else {
p = copy(p), spread(p);
if (t[p].val <= val)
x = p, split_v(t[p].rc, val, t[x].rc, y);
else
y = p, split_v(t[p].lc, val, x, t[y].lc);
upd(p);
}
}
void split_s(int p, int sze, int &x, int &y) {
if (!p)
x = y = 0;
else {
p = copy(p), spread(p);
if (t[t[p].lc].sze < sze)
x = p, split_s(t[p].rc, sze - t[t[p].lc].sze - 1, t[x].rc, y);
else
y = p, split_s(t[p].lc, sze, x, t[y].lc);
upd(p);
}
}
*/
int merge(int p, int q) {
if (!p || !q) return p ^ q;
if (t[p].key > t[q].key) {
spread(p);
t[p].rc = merge(t[p].rc, q), upd(p);
return p;
} else {
spread(q);
t[q].lc = merge(p, t[q].lc), upd(q);
return q;
}
}
/* 可持久化平衡树
int merge(int p, int q) {
if (!p || !q) return p ^ q;
if (rand() % (t[p].sze + t[q].sze) < t[p].sze) {
spread(p);
t[p].rc = merge(t[p].rc, q), upd(p);
return p;
} else {
spread(q);
t[q].lc = merge(p, t[q].lc), upd(q);
return q;
}
}
*/
}
笛卡尔树
- 每个节点由一个二元组 \((\mathrm{key}, \mathrm{val})\) 组成,其中键值 \(\mathrm{key}\) 满足 BST 性质,权值 \(\mathrm{val}\) 满足 heap 性质。
- 若每个节点的 \(\mathrm{key}, \mathrm{val}\) 均互不相同,则笛卡尔树的形态是唯一的。
建树:以大根堆为例,将所有元素按 \(\mathrm{key}\) 从小到大排序。每次插入的元素必然在该笛卡尔树的右链上(BST 性质),在右链查找第一个大于等于当前值的节点,将链拆成两部分,上半部分的下端接在当前节点的父亲上,下半部分的上端接在当前节点的左儿子上。
namespace CAR {
int root;
struct node {
int lc, rc;
} t[N];
int top, stk[N];
void build() {
for (int i = 1; i <= n; i ++) {
int k = top;
while (k && a[stk[k]] < a[i]) k --;
if (k) t[stk[k]].rc = i;
if (k < top) t[i].lc = stk[k + 1];
stk[top = k + 1] = i;
}
root = stk[1];
}
}
分块
Bn
为分块大小,t
为分块数量,Lp[], Rp[]
为当前块的左右端点,belong[]
为当前点所属块的编号。
int n, Bn;
int t, Lp[B_sze], Rp[B_sze], belong[N];
int main() {
for (int i = Bn; i <= n; i += Bn) t ++, Lp[t] = i - Bn + 1, Rp[t] = i;
if (Rp[t] < n) t ++, Lp[t] = Rp[t - 1] + 1, Rp[t] = n;
for (int i = 1; i <= n; i ++) belong[i] = (i - 1) / Bn + 1;
}
- 当遇到单点修改、区间查询时,可以考虑用分块优化到 \(\mathcal{O}(1)\) 修改 \(\mathcal{O}(\sqrt{n})\) 查询。
- 当遇到区间修改、单点查询时,可以考虑用分块优化到 \(\mathcal{O}(\sqrt{n})\) 修改 \(\mathcal{O}(1)\) 查询。
- 当遇到区间修改、区间查询时,可以考虑用差分转化为单点修改、区间查询。
- 当遇到限定 \(i \to i + k_i\),询问从 \(x\) 出发首次 \(\geq y\) 的行动步数时,可以预处理每个元素走出所在块后到达的点及所需步数。
- 当遇到区间 \([l, r]\) 的子区间 \([l', r'](l \leq l' \leq r' \leq r)\) 统计问题时,可以尝试分成以下几类讨论:
- \(l', r'\) 均在整块内。
- \(l', r'\) 中分别有一个在整块内、一个在散块内。
- \(l'\) 在左散块,\(r'\) 在右散块。
- \(l', r'\) 均在左散块或 \(l', r'\) 均在右散块。
- 特别要注意的是,在使用
sqrt()
前要调用#include <cmath>
。
莫队
普通莫队
- 最优排序方式等价于平面曼哈顿距离最短哈密顿路径,这是 NP 完全问题。
- 莫队排序方式:
- 第一关键字升序:左端点 \(l\) 所在块编号。
- 第二关键字升序:右端点 \(r\)。
- 奇偶性优化:当 \(\mathrm{belong}[l]\) 为奇数时,第二关键字升序;当 \(\mathrm{belong}[l]\) 为偶数时,第二关键字降序。
- 当左右端点扩展时,为避免出现 \(l > r\) 的情况,尽量保证区间先扩大,后缩小。
- 当 \(n, m\) 同阶时,分块大小 \(\mathcal{O}(\sqrt{n})\),时间复杂度 \(\mathcal{O}(n\sqrt{n})\)。
- 当 \(n, m\) 不同阶时,分块大小 \(\mathcal{O}(nm^{-\frac{1}{2}})\),时间复杂度 \(\mathcal{O}(nm^{\frac{1}{2}})\)。
int n, m;
int a[N];
int Bn;
int belong[N];
struct qry {
int l, r, id;
bool operator < (const qry &rhs) const {
if (belong[l] ^ belong[rhs.l]) return l < rhs.l;
return belong[l] & 1 ? r < rhs.r : r > rhs.r;
}
} q[M];
int ans[M];
int main() {
for (int i = 1; i <= m; i ++)
std::cin >> q[i].l >> q[i].r, q[i].id = i;
for (int i = 1; i <= n; i ++) belong[i] = (i - 1) / Bn + 1;
std::sort(q + 1, q + 1 + m);
int l = 1, r = 0;
for (int i = 1; i <= m; i ++) {
while (r < q[i].r) add(a[++ r]);
while (l > q[i].l) add(a[-- l]);
while (r > q[i].r) dec(a[r --]);
while (l < q[i].l) dec(a[l ++]);
// ans[q[i].id] = ...
}
}
高维莫队
- 对于 \(k\) 维莫队,对前 \(k - 1\) 维分块,第 \(i \in [1, k - 1]\) 关键字为第 \(i\) 维坐标所在块编号,第 \(k\) 关键字为第 \(k\) 维坐标。
- 分块大小 \(\mathcal{O}(nm^{-\frac{1}{k}})\),时间复杂度 \(\mathcal{O}(nm^{1 - \frac{1}{k}})\)。
带修莫队
- 普通莫队是不支持修改的。
- 可以给每个询问附加一个时间戳 \(t\),来表示回答该次询问时已经经过了多少次修改。转化为三维莫队。
- 当 \(n, m\) 同阶时,分块大小 \(\mathcal{O}(n^{\frac{2}{3}})\),时间复杂度 \(\mathcal{O}(n^{\frac{5}{3}})\)。
- 当 \(n, m\) 不同阶时,分块大小 \(\mathcal{O}(nm^{-\frac{1}{3}})\),时间复杂度 \(\mathcal{O}(nm^{\frac{2}{3}})\)。
int n, m;
int a[N];
int Bn;
int belong[N];
int mc;
struct cg {
int x, v;
} c[M];
int mq;
struct qry {
int l, r, t, id;
bool operator < (const qry &rhs) const {
if (belong[l] ^ belong[rhs.l]) return l < rhs.l;
if (belong[r] ^ belong[rhs.r]) return r < rhs.r;
return t < rhs.t;
}
} q[M];
int ans[M];
int main() {
for (int i = 1; i <= m; i ++) {
if (opt[0] == 'C') {
mc ++;
std::cin >> c[mc].x >> c[mc].v;
} else {
mq ++;
std::cin >> q[mq].l >> q[mq].r, q[mq].t = mc, q[mq].id = mq;
}
}
for (int i = 1; i <= n; i ++) belong[i] = (i - 1) / Bn + 1;
std::sort(q + 1, q + 1 + mq);
int l = 1, r = 0, t = 0;
for (int i = 1; i <= mq; i ++) {
while (r < q[i].r) add(a[++ r]);
while (l > q[i].l) add(a[-- l]);
while (r > q[i].r) dec(a[r --]);
while (l < q[i].l) dec(a[l ++]);
while (t < q[i].t) {
t ++;
if (q[i].l <= c[t].x && c[t].x <= q[i].r) dec(a[c[t].x]), add(c[t].v);
std::swap(a[c[t].x], c[t].v);
}
while (t > q[i].t) {
if (q[i].l <= c[t].x && c[t].x <= q[i].r) dec(a[c[t].x]), add(c[t].v);
std::swap(a[c[t].x], c[t].v);
t --;
}
// ans[q[i].id] = ...
}
}
回滚莫队
- 用于解决一类插入容易、删除不易的莫队问题。
- 当操作方便撤销时,可以考虑直接撤销。或是用栈记录操作,沿着栈撤销。
- 当操作不方便撤销时,可以考虑加入这些数对答案的影响。
int n, m;
int a[N];
int Bn;
int belong[N];
struct qry {
int l, r, id;
bool operator < (const qry &rhs) const {
if (belong[l] ^ belong[rhs.l]) return l < rhs.l;
return r < rhs.r;
}
} q[M];
int ans[M];
void add(int x, bool rem) {}
int main() {
for (int i = 1; i <= m; i ++)
std::cin >> q[i].l >> q[i].r, q[i].id = i;
for (int i = 1; i <= n; i ++) belong[i] = (i - 1) / Bn + 1;
std::sort(q + 1, q + 1 + m);
for (int x = 1, y = 1; x <= m; x = y = y + 1) {
while (y < m && belong[q[x].l] == belong[q[y + 1].l]) y ++;
int rightpos = belong[q[x].l] * Bn;
// init ...
int l = rightpos + 1, r = rightpos;
for (int i = x; i <= y; i ++) {
if (q[i].r <= rightpos) {
for (int j = q[i].l; j <= q[i].r; j ++) add(j, 1);
// ans[q[i].id] = ...
// Return to the last state.
} else {
while (r < q[i].r) add(++ r, 0);
// Record the current state.
while (l > q[i].l) add(-- l, 1);
// ans[q[i].id] = ...
// Return to the last state.
}
}
}
}
树上莫队
欧拉序:在 dfs 时,第一次访问到该节点时将该节点加入欧拉序(记作 \(\mathrm{In}_x\)),从该节点回溯时将该节点加入欧拉序(记作 \(\mathrm{Out}_x\))。
- 不妨设 \(\mathrm{In}_x < \mathrm{In}_y\):
- 若 \(\mathrm{LCA}(x, y) = x\),则从 \(x\) 到 \(y\) 的路径上的点为欧拉序 \([\mathrm{In}_x, \mathrm{In}_y]\) 中只出现过一次的点。
- 若 \(\mathrm{LCA}(x, y) \neq x\),则从 \(x\) 到 \(y\) 的路径上的点为欧拉序 \([\mathrm{Out}_x, \mathrm{In}_y]\) 中只出现过一次的点与 \(\mathrm{LCA}(x, y)\)。
int n, m;
int a[N];
int tot, head[N], ver[N * 2], Next[N * 2];
void add_edge(int u, int v) {
ver[++ tot] = v; Next[tot] = head[u]; head[u] = tot;
}
int dep[N];
int anc[logN + 1][N];
int In[N], Out[N];
int eul_len, eul[N * 2];
int Bn;
int belong[N * 2];
void dfs(int u, int fu) {
dep[u] = dep[fu] + 1;
anc[0][u] = fu;
for (int i = 1; i <= logN; i ++) anc[i][u] = anc[i - 1][anc[i - 1][u]];
eul[++ eul_len] = u, In[u] = eul_len;
for (int i = head[u]; i; i = Next[i]) {
int v = ver[i];
if (v == fu) continue;
dfs(v, u);
}
eul[++ eul_len] = u, Out[u] = eul_len;
}
int lca(int x, int y) {
if (dep[x] > dep[y]) std::swap(x, y);
for (int i = logN; i >= 0; i --)
if (dep[x] <= dep[anc[i][y]]) y = anc[i][y];
if (x == y) return x;
for (int i = logN; i >= 0; i --)
if (anc[i][x] != anc[i][y]) x = anc[i][x], y = anc[i][y];
return anc[0][x];
}
struct ask {
int l, r, z, id;
bool operator < (const qry &rhs) const {
if (belong[l] ^ belong[rhs.l]) return l < rhs.l;
return belong[l] & 1 ? r < rhs.r : r > rhs.r;
}
} q[M];
int ans[M];
bool state[N];
void attend(int p) {
state[p] ^= 1;
state[p] ? add(a[p]) : dec(a[p]);
}
int main() {
dfs(1, 0);
for (int i = 1, x, y, z; i <= m; i ++) {
std::cin >> x >> y, z = lca(x, y);
if (In[x] > In[y]) std::swap(x, y);
if (z == x)
q[i].l = In[x], q[i].r = In[y], q[i].z = 0, q[i].id = i;
else
q[i].l = Out[x], q[i].r = In[y], q[i].z = z, q[i].id = i;
}
for (int i = 1; i <= eul_len; i ++) belong[i] = (i - 1) / Bn + 1;
sort(q + 1, q + 1 + m, cmp);
int l = 1, r = 0;
for (int i = 1; i <= m; i ++) {
while (r < q[i].r) attend(eul[++ r]);
while (l > q[i].l) attend(eul[-- l]);
while (r > q[i].r) attend(eul[r --]);
while (l < q[i].l) attend(eul[l ++]);
if (q[i].z)
// calc ans[q[i].id] with q[i].z
else
// calc ans[q[i].id]
}
}
莫队二次离线
序列分治
最值分治
线段树分治
线段树优化建图
Trick
区间加、区间查询 转 单点加、区间查询
- 对于原序列 \(a\),记其差分序列为 \(b\)。
连续段计数
- 无重复元素:统计所有满足 \(\mathrm{\max} - \mathrm{min} = r - l\) 的区间个数。线段树 & 单调栈。
- 有重复元素:统计所有满足 \(\mathrm{\max} - \mathrm{min} + \mathrm{cnt} = r - l\) 的区间个数,其中 \(\mathrm{cnt}\) 为区间重复元素个数。维护每个位置上一个相同数的位置,线段树 & 单调栈。
D - 数据结构 树上结构
重链剖分
- 重链优先 dfs 序列:
- 任意子树在 dfs 序上,都对应一个连续区间。
- 任意重链在 dfs 序上,都对应一个连续区间。
- 向根的方向经过一条轻边后,所在子树大小至少乘 \(2\)。
- 任意一点到根路径上的所有边,可分成 \(\mathcal{O}(\log n)\) 个重路径的不交并。
- 任意两点之间路径上的所有边,可分成 \(\mathcal{O}(\log n)\) 个重路径的不交并。
int tot, head[N], ver[N * 2], Next[N * 2];
void add_edge(int u, int v) {
ver[++ tot] = v; Next[tot] = head[u]; head[u] = tot;
}
int dep[N], sze[N], Fa[N], son[N];
void dfs1(int u) {
dep[u] = dep[Fa[u]] + 1;
sze[u] = 1;
for (int i = head[u]; i; i = Next[i]) {
int v = ver[i];
if (v == Fa[u]) continue;
Fa[v] = u;
dfs1(v);
sze[u] += sze[v];
if (sze[v] > sze[son[u]]) son[u] = v;
}
}
int dfsClock, dfn[N], low[N];
int chain_top[N];
void dfs2(int u, int P) {
dfsClock ++;
dfn[u] = dfsClock, idx[dfsClock] = u;
chain_top[u] = P;
if (son[u]) dfs2(son[u], P);
for (int i = head[u]; i; i = Next[i]) {
int v = ver[i];
if (v == Fa[u] || v == son[u]) continue;
dfs2(v, v);
}
}
void chain_operate(int x, int y) {
while (chain_top[x] ^ chain_top[y]) {
if (dep[chain_top[x]] > dep[chain_top[y]]) std::swap(x, y);
// Operate on the [dfn[chain_top[y]], dfn[y]].
y = Fa[chain_top[y]];
}
if (dep[x] > dep[y]) std::swap(x, y);
// Operate on the [dfn[x], dfn[y]].
}
长链剖分
- 所有长链的长度和为 \(n\)。
- 任意一点到根路径上的所有边,可分成 \(\mathcal{O}(\sqrt{n})\) 个长路径的不交并。
int tot, head[N], ver[N * 2], Next[N * 2];
void add_edge(int u, int v) {
ver[++ tot] = v; Next[tot] = head[u]; head[u] = tot;
}
int dep[N], max_dep[N], Fa[N], son[N];
void dfs1(int u) {
max_dep[u] = dep[u] = dep[Fa[u]] + 1;
for (int i = head[u]; i; i = Next[i]) {
int v = ver[i];
if (v == Fa[u]) continue;
Fa[v] = u;
dfs1(v);
if (max_dep[v] > max_dep[u]) max_dep[u] = max_dep[v], son[u] = v;
}
}
int chain_top[N];
void dfs2(int u, int P) {
chain_top[u] = P;
if (son[u]) dfs2(son[u], P);
for (int i = head[u]; i; i = Next[i]) {
int v = ver[i];
if (v == Fa[u] || v == son[u]) continue;
dfs2(v, v);
}
}
长链剖分优化 dp:长链剖分可以优化附带深度维的 dp。
\(\mathcal{O}(n \log n) - \mathcal{O}(1)\) 树上 \(k\) 级祖先:
-
任意一点 \(x\) 的 \(k\) 级祖先,其所在的长链长度 \(\geq k\)。
-
对树进行长链剖分,预处理:
- 每个点的树上 \(2^k\) 级祖先。
- 每条长链(记该长链长度为 \(\mathrm{len}\))从长链顶端向上的 \(\mathrm{len}\) 个祖先和向下的 \(\mathrm{len}\) 个儿子。
- 每个二进制数的最高位。
-
每次询问 \(x\) 的 \(k\) 级祖先:
- 记 \(h\) 为 \(k\) 的最高二进制位。利用倍增数组先让 \(x\) 跳到 \(x\) 的 \(2^h\) 级祖先。
- 记剩余级数为 \(k' = k - 2^h\)。由于当前 \(x\) 所在长链长度 \(\mathrm{len} \geq 2^h \geq k - 2^h = k'\),可以先让 \(x\) 跳到 \(x\) 所在长链顶端,若剩余级数为正,则利用预处理的数组向上跳求出答案;若剩余级数为负,则利用预处理的数组向下跳求出答案。
int tot, head[N], ver[N * 2], Next[N * 2];
void add_edge(int u, int v) {
ver[++ tot] = v; Next[tot] = head[u]; head[u] = tot;
}
int dep[N], max_dep[N], Fa[N], son[N];
int anc[logN + 1][N];
void dfs1(int u) {
max_dep[u] = dep[u] = dep[Fa[u]] + 1;
anc[0][u] = Fa[u];
for (int i = 1; i <= logN; i ++) anc[i][u] = anc[i - 1][anc[i - 1][u]];
for (int i = head[u]; i; i = Next[i]) {
int v = ver[i];
if (v == Fa[u]) continue;
Fa[v] = u;
dfs1(v);
if (max_dep[v] > max_dep[u]) max_dep[u] = max_dep[v], son[u] = v;
}
}
int chain_top[N];
std::vector<int> up[N], dn[N];
void dfs2(int u, int P) {
chain_top[u] = P;
if (u == P) {
int len = max_dep[u] - dep[u] + 1;
for (int step = len, x = u; step; step --, x = Fa[x]) up[u].push_back(x);
for (int step = len, x = u; step; step --, x = son[x]) dn[u].push_back(x);
}
if (son[u]) dfs2(son[u], P);
for (int i = head[u]; i; i = Next[i]) {
int v = ver[i];
if (v == Fa[u] || v == son[u]) continue;
dfs2(v, v);
}
}
int logx[N];
int find(int x, int k) {
if (!k) return x;
int h = logx[k];
k -= (1 << h), x = f[x][h];
k -= dep[x] - dep[chain_top[x]], x = chain_top[x];
return k >= 0 ? up[x][k] : dn[x][-k];
}
int main() {
logx[0] = -1;
for (int i = 1; i <= n; i ++) logx[i] = logx[i >> 1] + 1;
// ...
}
LCT
- LCT 的辅助树由 splay 森林组成,每一棵 splay 都维护了一条偏爱链,splay 的中序遍历对应着偏爱链从链顶至链底。
- LCT 的每一棵 splay 的顶端节点,其父不认子。
- LCT 的操作注意事项:
find_root(x)
:在找到根之后,需要将根 splay 上去,否则复杂度可能会退化。link(x, y)
:在连边之前,需要判断 \(x, y\) 所在连通块的根是否不同。cut(x, y)
:在执行完make_root(x), access(y), splay(y)
之后,需要判断 \(y\) 的左子树是否有且仅有 \(x\)。
- LCT 经过
access
后,跨越非偏爱边的数量是 \(\mathcal{O}(\log n)\) 的。 - LCT 的
access
操作,可以实现从某节点到根路径上的标记覆盖,可以实现从某节点到根路径上标记的批量处理(因为每条实链的标记都是相同的)。由于根是不变的,就不必写翻转标记了。 - LCT 可以维护子树信息(需要满足可减性),需要维护虚子树的信息总和,只有
access
与link
操作需要改变。 - LCT 可以维护黑白树
- LCT 可以维护双连通分量。
- LCT 可以维护生成树。
namespace LCT {
struct node {
int son[2], Fa;
int val;
int sze;
bool rev;
#define lc son[0]
#define rc son[1]
void init(int x) {
lc = rc = 0, Fa = 0;
val = x;
sze = 1;
rev = 0;
}
void mk_rev() {
std::swap(lc, rc);
rev ^= 1;
}
} t[N];
void upd(int p) {
t[p].sze = t[t[p].lc].sze + t[t[p].rc].sze + 1;
}
void spread(int p) {
if (t[p].rev) {
if (t[p].lc) t[t[p].lc].mk_rev();
if (t[p].rc) t[t[p].rc].mk_rev();
t[p].rev = 0;
}
}
int dir(int x) {
return t[t[x].Fa].rc == x;
}
int is_top(int x) {
return t[t[x].Fa].lc != x && t[t[x].Fa].rc != x;
}
void rotate(int x) {
int y = t[x].Fa, z = t[y].Fa, k = dir(x);
t[x].Fa = z; if (!is_top(y)) t[z].son[t[z].rc == y] = x;
t[y].son[k] = t[x].son[k ^ 1], t[t[x].son[k ^ 1]].Fa = y;
t[x].son[k ^ 1] = y, t[y].Fa = x;
upd(y), upd(x);
}
void splay(int x) {
static int top, stk[N], p;
stk[top = 1] = x;
for (p = x; !is_top(p); p = t[p].Fa) stk[++ top] = t[p].Fa;
while (top) spread(stk[top --]);
while (p = t[x].Fa, !is_top(x)) {
if (!is_top(p)) rotate(dir(x) == dir(p) ? p : x);
rotate(x);
}
}
void access(int x) {
for (int y = 0; x; y = x, x = t[x].Fa) {
splay(x);
t[x].rc = y;
upd(x);
}
}
int find_root(int x) {
access(x), splay(x);
while (spread(x), t[x].lc) x = t[x].lc;
return splay(x), x;
}
void make_root(int x) {
access(x), splay(x), t[x].mk_rev();
}
void link(int x, int y) {
if (find_root(x) == find_root(y)) return;
make_root(x), t[x].Fa = y;
}
void cut(int x, int y) {
make_root(x), access(y), splay(y);
if (t[y].lc != x || t[x].rc) return;
t[y].lc = t[x].Fa = 0, upd(y);
}
}
namespace LCT {
const int pond = N + M;
int nClock;
struct node {
int son[2], Fa;
int val;
int max_id;
bool rev;
#define lc son[0]
#define rc son[1]
void init(int x, int p) {
lc = rc = 0, Fa = 0;
val = x;
max_id = p;
rev = 0;
}
void mk_rev() {
std::swap(lc, rc);
rev ^= 1;
}
} t[pond];
void upd(int p) {
t[p].max_id = p;
if (t[t[t[p].lc].max_id].val > t[t[p].max_id].val)
t[p].max_id = t[t[p].lc].max_id;
if (t[t[t[p].rc].max_id].val > t[t[p].max_id].val)
t[p].max_id = t[t[p].rc].max_id;
}
void spread(int p) {
if (t[p].rev) {
if (t[p].lc) t[t[p].lc].mk_rev();
if (t[p].rc) t[t[p].rc].mk_rev();
t[p].rev = 0;
}
}
int dir(int x) {
return t[t[x].Fa].rc == x;
}
bool is_top(int x) {
return t[t[x].Fa].lc != x && t[t[x].Fa].rc != x;
}
void rotate(int x) {
int y = t[x].Fa, z = t[y].Fa, k = dir(x);
t[x].Fa = z; if (!is_top(y)) t[z].son[t[z].rc == y] = x;
t[y].son[k] = t[x].son[k ^ 1], t[t[x].son[k ^ 1]].Fa = y;
t[x].son[k ^ 1] = y, t[y].Fa = x;
upd(y), upd(x);
}
void splay(int x) {
static int top, stk[pond], p;
stk[top = 1] = x;
for (p = x; !is_top(p); p = t[p].Fa) stk[++ top] = t[p].Fa;
while (top) spread(stk[top --]);
while (p = t[x].Fa, !is_top(x)) {
if (!is_top(p)) rotate(dir(x) == dir(p) ? p : x);
rotate(x);
}
}
void access(int x) {
for (int y = 0; x; y = x, x = t[x].Fa) {
splay(x);
t[x].rc = y;
upd(x);
}
}
int find_root(int x) {
access(x), splay(x);
while (spread(x), t[x].lc) x = t[x].lc;
return splay(x), x;
}
void make_root(int x) {
access(x), splay(x), t[x].mk_rev();
}
void link(int x, int y) {
make_root(x), t[x].Fa = y;
}
void extend(int x, int y, int z) {
t[++ nClock].init(z, nClock);
if (find_root(x) != find_root(y)) {
link(x, nClock), link(y, nClock);
} else {
make_root(x), access(y), splay(y);
if (t[t[y].max_id].val <= z) return;
int p = t[y].max_id;
splay(p), t[t[p].lc].Fa = t[t[p].rc].Fa = 0;
link(x, nClock), link(y, nClock);
}
}
}
int main() {
for (int i = 1; i <= n; i ++) LCT::t[i].init(-inf, i);
LCT::nClock = n;
// ...
}
namespace LCT {
struct node {
int son[2], Fa;
int sze;
int vir;
bool rev;
#define lc son[0]
#define rc son[1]
void init() {
lc = rc = 0, Fa = 0;
sze = 1;
vir = 0;
rev = 0;
}
void mk_rev() {
std::swap(lc, rc);
rev ^= 1;
}
} t[N];
void upd(int p) {
t[p].sze = t[t[p].lc].sze + t[t[p].rc].sze + 1 + t[p].vir;
}
void spread(int p) {
if (t[p].rev) {
if (t[p].lc) t[t[p].lc].mk_rev();
if (t[p].rc) t[t[p].rc].mk_rev();
t[p].rev = 0;
}
}
int dir(int x) {
return t[t[x].Fa].rc == x;
}
bool is_top(int x) {
return t[t[x].Fa].lc != x && t[t[x].Fa].rc != x;
}
void rotate(int x) {
int y = t[x].Fa, z = t[y].Fa, k = dir(x);
t[x].Fa = z; if (!is_top(y)) t[z].son[t[z].rc == y] = x;
t[y].son[k] = t[x].son[k ^ 1], t[t[x].son[k ^ 1]].Fa = y;
t[x].son[k ^ 1] = y, t[y].Fa = x;
upd(y), upd(x);
}
void splay(int x) {
static int top, stk[N], p;
stk[top = 1] = x;
for (p = x; !is_top(p); p = t[p].Fa) stk[++ top] = t[p].Fa;
while (top) spread(stk[top --]);
while (p = t[x].Fa, !is_top(x)) {
if (!is_top(p)) rotate(dir(x) == dir(p) ? p : x);
rotate(x);
}
}
void access(int x) {
for (int y = 0; x; y = x, x = t[x].Fa) {
splay(x);
t[x].vir += t[t[x].rc].sze - t[y].sze;
t[x].rc = y;
upd(x);
}
}
void make_root(int x) {
access(x), splay(x), t[x].mk_rev();
}
void link(int x, int y) {
if (find_root(x) == find_root(y)) return;
make_root(x), make_root(y);
t[x].Fa = y, t[y].vir += t[x].sze, t[y].sze += t[x].sze;
}
void cut(int x, int y) {
make_root(x), access(y), splay(y);
if (t[y].lc != x || t[x].rc) return;
t[y].lc = t[x].Fa = 0, upd(y);
}
}
点分治、点分树
- 在点分治过程中,将本层中与上一层重心相邻的点记作本层的根。若将以 " 上一层的根 " 作为根时的子树大小作为本层总节点数,虽然在本层的根在上一层的根与上一层的重心简单路径上时,本层总结点数会计算错误,但不影响复杂度。
int tot, head[N], ver[N * 2], Next[N * 2];
void add_edge(int u, int v) {
ver[++ tot] = v; Next[tot] = head[u]; head[u] = tot;
}
bool ban[N];
int sz[N], mp[N];
int A_sz, A_rt;
void Get_rt(int u, int fu) {
sz[u] = 1, mp[u] = 0;
for (int i = head[u]; i; i = Next[i]) {
int v = ver[i];
if (v == fu || ban[v]) continue;
Get_rt(v, u);
sz[u] += sz[v];
if (sz[v] > mp[u]) mp[u] = sz[v];
}
if (A_sz - sz[u] > mp[u]) mp[u] = A_sz - sz[u];
if (A_rt == 0 || mp[u] < mp[A_rt]) A_rt = u;
}
void solve(int u) {
ban[u] = 1;
// Calculate the path through point u.
for (int i = head[u]; i; i = Next[i]) {
int v = ver[i];
if (ban[v]) continue;
A_sz = sz[v], A_rt = 0;
Get_rt(v, 0), solve(A_rt);
}
}
int main() {
A_sz = n, A_rt = 0;
Get_rt(1, 0), solve(A_rt);
}
Trick
换根
- 钦定以 \(1\) 为根,考虑换根对信息的影响。
- 设当前的根为 \(\mathcal{R}\):
- 若 \(x\) 为 \(\mathcal{R}\),则以 \(\mathcal{R}\) 为根时 \(x\) 的子树,为整棵树。
- 若 \(x\) 为 \(\mathcal{R}\) 的祖先,则以 \(\mathcal{R}\) 为根时 \(x\) 的子树,为整棵树刨去以 \(1\) 为根时 \(s\) 的子树,其中 \(s\) 为 \(x\) 向 \(\mathcal{R}\) 方向上的儿子。
- 若是其他情况,则以 \(\mathcal{R}\) 为根时 \(x\) 的子树,为以 \(1\) 为根时 \(x\) 的子树。
- 设当前的根为 \(\mathcal{R}\),则以 \(\mathcal{R}\) 为根时的 \(\mathrm{LCA}(x, y)\),为以 \(1\) 为根时 \(\mathrm{LCA}(x, y), \mathrm{LCA}(x, \mathcal{R}), \mathrm{LCA}(y, \mathcal{R})\) 中深度最大者。