[学习笔记] 析合树学习笔记
作者:互联网
做 CF 的时候碰到了这个,于是就滚过来学了。本文全部参考 OI-wiki。所以你的学习笔记就是把原文抄一遍吗
其实是一个比较简单的东西,感觉 OI-wiki 上有些部分写得太繁杂了,所以就稍微做了一些简化。
引入
这个问题是这样的:给定一个长度为 \(n\) 的排列,我们称一个值域连续的区间为段,问一个排列的段的个数。析合树就是用来解决这类“值域连续的区间段”问题。
连续段
以下给出一些定义。
对于排列 \(p\),定义连续段 \((P,[l,r])\) 表示一个区间 ,要求 \(P_{l\sim r}\) 值域是连续的。特别地,当 \(l > r\) 时我们认为这是一个空的连续段,记作 \((P,\varnothing)\)。我们称排列 \(P\) 的所有连续段的集合为 \(I_P\),并且我们认为 \((P,\varnothing) \in I_P\)。
可以定义连续段的交并差的运算。但其实就是普通的集合交并差放在区间上而已,所以不赘述了。
连续段有一些显而易见的性质。我们定义 \(A,B \in I_P,A \cap B \neq \varnothing,A \notin B, B \notin A\),那么有 \(A \cup B,A \cap B,A \ B,B\ A \in I_P\)。证明只需要考虑集合交并差的运算即可。
析合树
析合树正是由连续段组成的一棵树。但一个排列可能有多达 \(O(n^2)\) 个连续段,因此我们需要找出其中更基本的连续段组成析合树。
本原段
对于排列 \(P\),我们认为连续段 \(M\) 是一个本原段当且仅当在 \(I_P\) 中不存在与之相交且与 \(M\) 没有包含关系的连续段。记所有连续段的集合为 \(M_P\),显然 \((P,\varnothing) \in M_P\)。
显然,本原段之间只有相离或者包含关系,并且一个连续段可以由几个互不相交的本原段构成。最大的本原段就是整个排列本身,它包含了其他所有本原段,因此本原段可以构成一个树形结构,我们称这个结构为析合树。更严格地说,排列 \(P\) 的析合树由排列 \(P\) 的所有本原段组成。
举个例子,对于排列 \(P = \{ 9,1,10,3,2,5,7,6,8,4 \}\),它的本原段构成的析合树如下:
图中每个结点都代表一个本原段,但我们只标出了每个段的值域。图中提到了析点与合点,那么什么是析点与合点呢?
析点与合点
以下给出定义:
- 值域区间:对于一个结点 \(u\),用 \([u_l,u_r]\) 表示该结点的值域区间。
- 儿子序列:对于析合树上的一个结点 \(u\),假设它的儿子结点是一个有序序列,该序列以值域区间为元素。我们把这个序列称为儿子序列,记作 \(S_u\)。
- 儿子排列:对于一个儿子序列 ,把它的元素离散化成正整数后形成的排列称为儿子排列。结点 \(u\) 的儿子排列记为 \(P_u\)。
- 合点:我们认为,儿子排列为顺序或者逆序的点为合点。叶子结点没有儿子排列,我们也认为它是合点。
- 析点:不是合点的就是析点。
析点与合点的性质
析点与合点的命名来源于他们的性质。有一个非常显然的性质:对于析合树中任何的结点 \(u\),其儿子序列区间的并集就是结点 \(u\) 的值域区间。事实上析点和合点具有更好的性质:对于一个合点,其儿子序列的任意 子区间都构成一个连续段;而对于一个析点,其儿子序列的任意长度大于 \(1\) 的子区间都不构成一个连续段。
合点的性质不难证明。对于析点性质的证明我们使用反证法,假设对于一个点 \(u\),它的儿子序列中有一个最长的区间 \(S_{u,l \sim r}\) 构成了连续段 \(A\)。那么 \(A = \bigcup_{i=l}^r S_{u,i}\),也就意味着 \(A\) 是一个本原段。这和析合树使用了所有的本原段矛盾。这就证明了性质。
析合树的构造
以下介绍析合树的 \(O(n \log n)\) 构造方法。
我们考虑增量法。用一个栈维护前 \(i-1\) 个元素构成的析合森林。现在考虑当前结点 \(P_i\)。
- 判断其能否成为栈顶结点的儿子,如果能,将其设为栈顶的儿子,然后把栈顶取出作为当前结点。重复上述过程直到栈空或者不能成为栈顶结点的儿子。
- 如果不能成为栈顶的儿子,判断把栈顶的若干个连续的结点合并成一个结点,然后把合并后的点作为当前结点。
- 重复上述过程直到不能进行为止。然后结束此次增量,把当前结点压栈。
接下来我们对此过程给出更加详细的解释。
如果当前点能够成为栈顶结点的儿子,那么栈顶结点是一个合点。如果是栈顶结点是析点,那么合并后这个析点就存在一个子连续段,不满足析点的性质。因此一定是合点。
如果无法成为栈顶结点的儿子,那么我们需要判断栈顶连续的若干个点能否与当前点一起合并。设 \(l\) 为当前点所在区间的左端点。我们计算 \(L_i\) 表示右端点下标为 \(i\) 的连续段中,左端点 \(l\) 的最小值。记栈顶结点为 \(t\)。
- 如果 \(L_i\) 不存在,那么显然当前结点无法合并。
- 如果 \(t_l = L_i\),那么这两个结点合并,合并后是一个合点。
- 否则在栈中一定存在一个点 \(t'\) 的左端点 \(t'_l = L_i\),那么一定可以从当前结点合并到 \(t'\) 形成一个析点。
现在的问题是我们如何求出 \(L_i\)。我们容易得出一个区间 \(P_{l \sim r}\) 是连续段当且仅当:
\[\max_{l \leq i \leq r} P_i - \min_{l \leq i \leq r} P_i = r - l \]而由于 \(P\) 是一个排列,因此对于任意的区间 \([l,r]\) 都有:
\[\max_{l \leq i \leq r} P_i - \min_{l \leq i \leq r} P_i \geq r - l \]维护 \(\max_{l \leq i \leq r} P_i - \min_{l \leq i \leq r} P_i - (r-l)\),那么找到一个连续段相当于查询最小值。具体地,对于增量过程中的 \(i\),我们维护一个数组 \(Q\) 表示区间 \([j,i]\) 的极差减长度。即
\[Q_j = \max_{j \leq k \leq i} P_k - \min_{j \leq k \leq i} P_k - (i-j) \]现在我们想知道在 \(1 \sim i-1\) 中是否存在一个最小的 \(j\) 使得 \(Q_j=0\)。这等价于求 \(Q_{1 \sim i-1}\) 的最小值。求得最小的 \(j\) 就是 \(L_i\)。如果这样的 \(j\) 不存在,那么 \(L_i = i\)。
对于 \(Q\) 的维护可以按照如下步骤进行:
- 找到最大的 \(j\) 使得 \(P_j > P_{i+1}\),对于 \(Q_{j+1 \sim i}\) 需要更新 \(\max\) 的贡献,做区间加即可。
- 更新 \(\min\) 同理。
- \(Q\) 全局减 \(1\),这是因为区间长度加 \(1\)。
- 查询 \(Q\) 最小值所在的下标。
单调栈+线段树即可,具体维护方式见例题的代码实现。
例题 [CERC2017] Intrinsic Interval
给定一个长度为 \(n\) 的排列 \(p\),\(q\) 次询问包含 \([l,r]\) 的最短连续段。
思路
对连续段建析合树。每次询问只需要找两个节点的 lca,如果 lca 是析点那么答案就是该析点,否则因为合点的儿子序列的任一子区间都是连续段,只需要取最小的那个子区间即可。
代码
/*
也许所有的执念 就像四季的更迭
没有因缘 不需致歉
是否拥抱着告别 就更能读懂人间
还是感慨 更多一点
*/
#include <bits/stdc++.h>
#define pii pair<int, int>
#define mp(x, y) make_pair(x, y)
#define pb push_back
#define fi first
#define se second
#define int long long
#define mem(x, v) memset(x, v, sizeof(x))
#define mcpy(x, y, n) memcpy(x, y, sizeof(int) * (n))
#define lob lower_bound
#define upb upper_bound
using namespace std;
inline int read() {
int x = 0, w = 1;char ch = getchar();
while (ch > '9' || ch < '0') { if (ch == '-')w = -1;ch = getchar(); }
while (ch >= '0' && ch <= '9') x = x * 10 + ch - '0', ch = getchar();
return x * w;
}
inline int min(int x, int y) { return x < y ? x : y; }
inline int max(int x, int y) { return x > y ? x : y; }
const int MN = 2e5 + 5;
const int Mod = 1e9 + 7;
const int Inf = 2e18;
inline void Add(int &x, int y) { x += y; if (x >= Mod) x -= Mod; }
inline void Dec(int &x, int y) { x -= y; if (x < 0) x += Mod; }
inline int qPow(int a, int b = Mod - 2, int ret = 1) {
while (b) {
if (b & 1) ret = ret * a % Mod;
a = a * a % Mod, b >>= 1;
}
return ret;
}
// #define dbg
int N, M, a[MN], stk1[MN], stk2[MN], tp1, tp2, rt;
int L[MN], R[MN], vr[MN], id[MN], cnt, ty[MN], bin[20], stk[MN], tp;
struct RMQ {
int lg[MN], mx[MN][17], mn[MN][17];
inline void Build() {
for (int i = bin[0] = 1; i < 20; i++) bin[i] = bin[i - 1] << 1;
for (int i = 2; i <= N; i++) lg[i] = lg[i >> 1] + 1;
for (int i = 1; i <= N; i++) mx[i][0] = mn[i][0] = a[i];
for (int i = 1; i < 17; i++)
for (int j = 1; j + bin[i] - 1 <= N; j++)
mn[j][i] = min(mn[j][i - 1], mn[j + bin[i - 1]][i - 1]),
mx[j][i] = max(mx[j][i - 1], mx[j + bin[i - 1]][i - 1]);
}
inline int qrymn(int l, int r) {
int t = lg[r - l + 1];
return min(mn[l][t], mn[r - bin[t] + 1][t]);
}
inline int qrymx(int l, int r) {
int t = lg[r - l + 1];
return max(mx[l][t], mx[r - bin[t] + 1][t]);
}
} D;
const int MS = MN << 2;
#define ls o << 1
#define rs o << 1 | 1
#define mid ((l + r) >> 1)
#define LS ls, l, mid
#define RS rs, mid + 1, r
struct SGT {
int mn[MS], tg[MS];
inline void pushup(int o) {
mn[o] = min(mn[ls], mn[rs]);
}
inline void upd(int o, int v) {
mn[o] += v, tg[o] += v;
}
inline void pushdown(int o) {
if (tg[o]) upd(ls, tg[o]), upd(rs, tg[o]), tg[o] = 0;
}
inline void Mdf(int o, int l, int r, int L, int R, int v) {
if (r < L || l > R) return;
if (L <= l && R >= r) return upd(o, v), void();
pushdown(o);
Mdf(LS, L, R, v), Mdf(RS, L, R, v), pushup(o);
}
inline int Qry(int o, int l, int r) {
if (l == r) return l;
pushdown(o);
return (!mn[ls] ? Qry(LS) : Qry(RS));
}
} T;
int dep[MN], fa[MN][20];
vector <int> G[MN];
inline void DFS(int u) {
for (int i = 1; bin[i] <= dep[u]; i++) fa[u][i] = fa[fa[u][i - 1]][i - 1];
for (int v : G[u]) {
dep[v] = dep[u] + 1;
fa[v][0] = u;
DFS(v);
}
}
inline int climb(int u, int d) {
for (int i = 0; i < 18 && d; i++)
if (bin[i] & d) d ^= bin[i], u = fa[u][i];
return u;
}
inline int LCA(int u, int v) {
if (dep[u] < dep[v]) swap(u, v);
u = climb(u, dep[u] - dep[v]);
if (u == v) return u;
for (int i = 17; ~i; i--)
if (fa[u][i] != fa[v][i]) u = fa[u][i], v = fa[v][i];
return fa[u][0];
}
inline int chk(int l, int r) {
return D.qrymx(l, r) - D.qrymn(l, r) == r - l;
}
inline void Build() {
for (int i = 1; i <= N; i++) {
while (tp1 && a[i] <= a[stk1[tp1]])
T.Mdf(1, 1, N, stk1[tp1 - 1] + 1, stk1[tp1], a[stk1[tp1]]), tp1--;
while (tp2 && a[i] >= a[stk2[tp2]])
T.Mdf(1, 1, N, stk2[tp2 - 1] + 1, stk2[tp2], -a[stk2[tp2]]), tp2--;
T.Mdf(1, 1, N, stk1[tp1] + 1, i, -a[i]);
stk1[++tp1] = i;
T.Mdf(1, 1, N, stk2[tp2] + 1, i, a[i]);
stk2[++tp2] = i;
id[i] = ++cnt;
L[cnt] = R[cnt] = i;
int pr = T.Qry(1, 1, N), cur = cnt;
while (tp && L[stk[tp]] >= pr) {
if (ty[stk[tp]] && chk(vr[stk[tp]], i)) {
R[stk[tp]] = i, vr[stk[tp]] = L[cur], G[stk[tp]].pb(cur), cur = stk[tp--];
} else if (chk(L[stk[tp]], i)) {
ty[++cnt] = 1, L[cnt] = L[stk[tp]], R[cnt] = i, vr[cnt] = L[cur];
G[cnt].pb(stk[tp--]), G[cnt].pb(cur);
cur = cnt;
} else {
G[++cnt].pb(cur);
do G[cnt].pb(stk[tp--]);
while (tp && !chk(L[stk[tp]], i));
L[cnt] = L[stk[tp]], R[cnt] = i, G[cnt].pb(stk[tp--]);
cur = cnt;
}
}
stk[++tp] = cur;
T.Mdf(1, 1, N, 1, i, -1);
}
rt = stk[1];
}
inline void Qry(int l, int r) {
int x = id[l], y = id[r];
int z = LCA(x, y);
if (ty[z] & 1)
l = L[climb(x, dep[x] - dep[z] - 1)], r = R[climb(y, dep[y] - dep[z] - 1)];
else
l = L[z], r = R[z];
printf("%lld %lld\n", l, r);
}
signed main(void) {
N = read();
for (int i = 1; i <= N; i++) a[i] = read();
D.Build();
Build();
DFS(rt);
M = read();
for (int i = 1; i <= M; i++) {
int x = read(), y = read();
Qry(x, y);
}
return 0;
}
标签:结点,int,cnt,tp,stk,学习,leq,笔记,析合树 来源: https://www.cnblogs.com/came11ia/p/16463526.html