2025.9.29 闲话-析合树

9.29 闲话-析合树

Part.1 析合树概念

该树是一类维护排列的连续段的数据结构,复杂度最优可以做到 \(O(n)\) 不过平民还是写 \(O(n\log n)\) 吧。

先引入一个问题:

求一个排列所有的连续段个数。

首先定义什么是连续段,就像名字一样是连续的段也就是对于 \([l,r]\) 的元素进行升序排序后 \(a_i-a_{i-1}=1\)

更加好维护的方式是:

\[\max_{i=l}^r{a_i}-\min_{i=l}^r{a_i}=r-l \]

可以发现连续段的数量肯定是 \(O(n^2)\) 级别的,那么直接维护肯定爆了,这时候就要使用析合树维护了。

首先定义本源连续段为其他任何一个连续段都不和其严格相交(也就是只能包含或相离)。

这时候通过本源连续段的包含关系,便建出了一颗树形结构,这个树形结构就是析合树了。

析合树的重点肯定是析和合啦。

定义合点为儿子的值域从左到右是单调的,这样任取一段儿子都能构成一个合法连续段。

这时候合点的贡献就是任意两个儿子的这一段加这个点了,怎么证?因为如果有跨越两个点的贡献,就说明这个点不是本源连续段!

那么不是合点的点就是析点咯,析点最重要的就是其任意一段儿子都不能构成连续段,因为要是有则就说明有本源连续段不在析合树上。

所以只要成功建立了析合树,就可以完美统计答案了。

咋建?

Part.2 析合树建立

考虑维护一个析合树森林,将所有的根放入栈中。

  1. 如果栈顶点为合点,且当前点可以作为其儿子,则直接并入其中。
  2. 如果不能作为儿子,则找到最靠右的一段满足这一段根可以构成一个新连续段,如果为栈顶则新点为合点,否则为析点。

首先怎么判儿子呢?发现并入其中的唯一判断便就是是否能和最右边儿子构成连续段,直接维护就好了,判断就是用一开始的式子用 ST 表维护就行。

那么现在怎么找到最靠右的一段满足这一段根可以构成一个新连续段呢?

这时候就需要使用些手段了。

因为有:

\[\max_{i=l}^r{a_i}-\min_{i=l}^r{a_i}=r-l \]

所以有:

\[\max_{i=l}^r{a_i}-\min_{i=l}^r{a_i}-r+l=0 \]

又因为:

\[\max_{i=l}^r{a_i}-\min_{i=l}^r{a_i}\ge r-l \]

于是我们要找的合法段就变成了区间的最小值,这时候只需要进行线段树二分就好了。

\(max\)\(min\) 的变化发现一段后缀将最小最大值更改为了 \(a_i\) 那么只需要使用单调栈进行区间修改就成了。

复杂度 \(O(n\log n)\)

点击查看代码
#include <iostream>
#include <vector>

using namespace std;

#define emp emplace_back

const int N = 2e5 + 10;

using pii = pair <int, int>;
using ll = long long;

int a[N], n;

class Stable
{
    public :

    int lg[N], minst[25][N], maxst[25][N];

    void Build()
    {
        for (int i = 1; i <= n; i++) minst[0][i] = maxst[0][i] = a[i];
        for (int i = 2; i <= n; i++) lg[i] = lg[i >> 1] + 1;
        for (int i = 1; i <= 20; i++)
        {
            for (int j = 1; j + (1 << i) - 1 <= n; j++)
            {
                minst[i][j] = min(minst[i - 1][j], minst[i - 1][j + (1 << (i - 1))]);
                maxst[i][j] = max(maxst[i - 1][j], maxst[i - 1][j + (1 << (i - 1))]);
            }
        }
    }

    int QueryMin(int l, int r)
    {
        int k = lg[r - l + 1];
        return min(minst[k][l], minst[k][r - (1 << k) + 1]);
    }

    int QueryMax(int l, int r)
    {
        int k = lg[r - l + 1];
        return max(maxst[k][l], maxst[k][r - (1 << k) + 1]);
    }
};

class SemTree
{
    #define lid id << 1
    #define rid id << 1 | 1
    public :

    int val[N << 2], tag[N << 2];

    void PushDown(int id)
    {
        if (tag[id])
        {
            tag[lid] += tag[id], tag[rid] += tag[id], val[lid] += tag[id], val[rid] += tag[id];
            tag[id] = 0;
        }
    }

    void PushUp(int id) {val[id] = min(val[lid], val[rid]);}

    void Update(int id, int cl, int cr, int l, int r, int k)
    {
        if (l <= cl && cr <= r) return val[id] += k, tag[id] += k, void();
        PushDown(id);
        int mid = (cl + cr) >> 1;
        if (l <= mid) Update(lid, cl, mid, l, r, k);
        if (r > mid) Update(rid, mid + 1, cr, l, r, k);
        PushUp(id);
    }
    
    int Find(int id, int l, int r)
    {
        if (l == r) return l;
        PushDown(id);
        int mid = (l + r) >> 1;
        if (val[lid]) return Find(rid, mid + 1, r);
        else return Find(lid, l, mid);
    }
};

vector <int> G[N];
void add(int x, int y) {G[x].emp(y);}

class XHTree
{
    public :

    Stable ST;
    SemTree T;
    int L[N << 1], R[N << 1], M[N << 1], f[N << 1], v[N << 1], rt, cnt;
    int st[N], st1[N], st2[N], top, top1, top2;

    bool Check(int l, int r) {return ST.QueryMax(l, r) - ST.QueryMin(l, r) == r - l;}

    void Build()
    {
        ST.Build();
        for (int i = 1; i <= n; i++) T.Update(1, 1, n, i, i, i);
        for (int i = 1; i <= n; i++)
        {
            T.Update(1, 1, n, 1, n, -1);
            while (top1 && a[st1[top1]] < a[i]) T.Update(1, 1, n, st1[top1 - 1] + 1, st1[top1], -a[st1[top1]]), --top1;
            while (top2 && a[st2[top2]] > a[i]) T.Update(1, 1, n, st2[top2 - 1] + 1, st2[top2], a[st2[top2]]), --top2;
            T.Update(1, 1, n, st1[top1] + 1, i, a[i]); st1[++top1] = i;
            T.Update(1, 1, n, st2[top2] + 1, i, -a[i]); st2[++top2] = i;
            int now = ++cnt, l = T.Find(1, 1, n);
            L[now] = R[now] = i;
            while (top && L[st[top]] >= l)
            {
                if (v[st[top]] && Check(M[st[top]], i)) f[now] = st[top], M[st[top]] = L[now], R[st[top]] = i, now = st[top--];
                else if (Check(L[st[top]], i))
                {
                    v[++cnt] = 1, f[st[top]] = f[now] = cnt;
                    L[cnt] = L[st[top]], R[cnt] = i, M[cnt] = L[now];
                    --top, now = cnt;
                }
                else
                {
                    R[++cnt] = now, f[now] = cnt;
                    while (top && !Check(L[st[top]], i)) f[st[top--]] = cnt;
                    L[cnt] = L[st[top]], f[st[top--]] = cnt;
                    now = cnt;
                }
            }
            st[++top] = now;
        }
        rt = st[1];
        for (int i = 1; i <= cnt; i++) if (f[i]) add(f[i], i);
    }

    ll Calc(int x)
    {
        ll tot = 0, ans = 0;
        for (auto y : G[x]) ans += Calc(y), ++tot;
        return v[x] * (tot * (tot - 1) / 2ll) + !v[x] + ans;
    }
}T;

signed main()
{
    // freopen("data.in", "r", stdin); freopen("data.out", "w", stdout);
    ios :: sync_with_stdio(false), cin.tie(0), cout.tie(0);

    cin >> n;
    for (int i = 1; i <= n; i++) cin >> a[i];
    T.Build();
    cout << T.Calc(T.rt) << '\n';

    return 0;
}

Part.3 析合树计数

本段参考了 xrlong's blog

真正建析合树的题目真是少之又少,因为树上问题绝对没有 \(dp\) + 多项式有杀伤性,更多考察的是析合树计数问题。

用一个比较典的问题来举例子:

求一个 \(n\) 阶排列不包含任何真连续段的方案数。

其实这个问题等价于求一个根为析点并且所有儿子都是叶子的方案数。

首先考虑删去所有合点的方案,这里考虑枚举第一个儿子大小。

发现如果第一个儿子出现了一段 \(1\) 的前缀连续段,那么就肯定有后面构成一个段使得这个儿子不是本源连续段,假了,那么就要记录没有 \(1\) 前缀连续段的方案数。

假设长度 \(i\)\(g_i\) 则有:

\[g_i=i!-\sum_{j=1}^{i-1}{g_j(i-j)!} \]

因为每个不合法排列都可以通过最左边的合法排列删去。

此时就发现这个合点的方案是:

\[2\sum_{i=1}^{n-1}{g_i(n-i)!} \]

因为后面的首先不会并入这个儿子,其次不合法的一定会优先合并成为一块成为合法的,所以后面的任意排列即可。

假设 \(G_{i,j}\) 表示将 \(i\) 个数划分为 \(j\) 个连续段的方案数。

有:

\[G_{i,j}=\sum_{k=1}^{i-1}{G_{i-k,j-1}k!} \]

假设 \(f_i\) 表示 \(i\) 个点构成的不包含任何真连续段的方案数,那么:

\[f_n=n!-2\sum_{i=1}^{n-1}{g_i(n-i)!}-\sum_{i=3}^{n-1}{G_{n,i}f_i} \]

因为一个点如果是合法点,则一定有 \(n\) 个儿子,那么答案就是删去合点并删去不是 \(n\) 个儿子的析点。

这样复杂度就是 \(O(n^3)\) 的了。

luogu P7278 纯洁憧憬

首先要求的是存在长度大于 \(k\) 的非平凡连续段。

分析上面的求解过程,发现合点的情况比较好处理,只需要删去所有小于等于 \(k\) 的合点,发现只可能出现在前后或减去前后的四个连续段中,那么就直接枚举第一个和最后一个儿子的大小,大小在 \([n-k,k]\) 之间就行。

其次是析点的情况,\(G_{i,j}\) 的划分本质上就已经揭示了这个析点的形态,那么只需要将 \(G_{i,j}\) 更改为划分为长度不超过 \(k\)\(j\) 个连续段就行,设为 \(F_{i,j}\)

那么答案为:

\[n!-\sum_{i=n-k}^{k}{\sum_{j=n-k}^{k}{g_ig_j(n-i-j)!}}-\sum_{i=3}^{n}{F_{n,i}f_i} \]

上述过程可以使用多项式优化到 \(O(n\log^2 n)\)(我不会)。

posted @ 2025-09-29 22:13  QEDQEDQED  阅读(22)  评论(0)    收藏  举报