其他分享
首页 > 其他分享> > [学习笔记] 析合树学习笔记

[学习笔记] 析合树学习笔记

作者:互联网

做 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 \}\),它的本原段构成的析合树如下:

图中每个结点都代表一个本原段,但我们只标出了每个段的值域。图中提到了析点与合点,那么什么是析点与合点呢?

析点与合点

以下给出定义:

  1. 值域区间:对于一个结点 \(u\),用 \([u_l,u_r]\) 表示该结点的值域区间。
  2. 儿子序列:对于析合树上的一个结点 \(u\),假设它的儿子结点是一个有序序列,该序列以值域区间为元素。我们把这个序列称为儿子序列,记作 \(S_u\)。
  3. 儿子排列:对于一个儿子序列 ,把它的元素离散化成正整数后形成的排列称为儿子排列。结点 \(u\) 的儿子排列记为 \(P_u\)。
  4. 合点:我们认为,儿子排列为顺序或者逆序的点为合点。叶子结点没有儿子排列,我们也认为它是合点。
  5. 析点:不是合点的就是析点。

析点与合点的性质

析点与合点的命名来源于他们的性质。有一个非常显然的性质:对于析合树中任何的结点 \(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\)。

  1. 判断其能否成为栈顶结点的儿子,如果能,将其设为栈顶的儿子,然后把栈顶取出作为当前结点。重复上述过程直到栈空或者不能成为栈顶结点的儿子。
  2. 如果不能成为栈顶的儿子,判断把栈顶的若干个连续的结点合并成一个结点,然后把合并后的点作为当前结点。
  3. 重复上述过程直到不能进行为止。然后结束此次增量,把当前结点压栈。

接下来我们对此过程给出更加详细的解释。

如果当前点能够成为栈顶结点的儿子,那么栈顶结点是一个合点。如果是栈顶结点是析点,那么合并后这个析点就存在一个子连续段,不满足析点的性质。因此一定是合点。

如果无法成为栈顶结点的儿子,那么我们需要判断栈顶连续的若干个点能否与当前点一起合并。设 \(l\) 为当前点所在区间的左端点。我们计算 \(L_i\) 表示右端点下标为 \(i\) 的连续段中,左端点 \(l\) 的最小值。记栈顶结点为 \(t\)。

  1. 如果 \(L_i\) 不存在,那么显然当前结点无法合并。
  2. 如果 \(t_l = L_i\),那么这两个结点合并,合并后是一个合点。
  3. 否则在栈中一定存在一个点 \(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\) 的维护可以按照如下步骤进行:

  1. 找到最大的 \(j\) 使得 \(P_j > P_{i+1}\),对于 \(Q_{j+1 \sim i}\) 需要更新 \(\max\) 的贡献,做区间加即可。
  2. 更新 \(\min\) 同理。
  3. \(Q\) 全局减 \(1\),这是因为区间长度加 \(1\)。
  4. 查询 \(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