ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

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

2022-07-10 22:00:32  阅读:164  来源: 互联网

标签:结点 int cnt tp stk 学习 leq 笔记 析合树


做 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

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有